Vue2.1.7 Source Learning (I)

Posted by skroks609 on Mon, 01 Jul 2019 22:10:35 +0200

The title of the original article is "Source Code Analysis", but later on, I think it is appropriate to use "source code learning". Before I have a thorough grasp of each letter in the source code, "parsing" is a bit of a headline party. It is suggested that before reading this article, it is better to open the source code of 2.1.7 for comparison, which may be easier to understand. In addition, my level is limited, there are errors or inappropriate places in this article, I hope you can make more corrections and grow together.

Supplement: Vue 2.2 has just been released. As the first article in this series, this article focuses on the organization of Vue code, the restoration of Vue constructors, the prototype design, the processing of parameter options, the data binding that has been written out and how to update views using Virtual DOM. From the overall perspective of the framework, it seems that V2.1.7 has little impact on understanding the code of V2.2. Follow-up articles in this series will start with the latest source code and give corresponding tips on the changes.

I wrote an article a long time ago: When JavaScript implements MVVM, I just want to monitor the changes of a common object. At the beginning of the article, I mentioned my style of blogging, or that sentence. I only wrote articles that I tried to make Xiaobai, even the pupils, understand. This will inevitably lead to some ink marks in this article for some students, so you can read it in detail or jump to read it according to your preferences.

I. Start with an understanding of an open source project

To look at the source code of a project, don't look at it from the first place, first to understand the metadata and dependencies of the project itself, in addition to the best understanding of PR rules, Issue Reporting rules and so on. Especially for the "front-end" open source project, before we look at the source code, the first thought should be: package.json file.

In the package.json file, we should pay more attention to the scripts field and devendencies and dependencies field. Through the scripts field, we can know the script commands defined in the project. Through the devendencies and dependencies field, we can know the dependencies of the project.

With this in mind, if we have dependencies, we can install dependencies with npm install.

In addition to package.json, we also need to read the contribution rule document of the project to understand how to start. A good open source project will definitely include this part of the content, Vue is no exception: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md In this document, some codes of conduct, PR guidelines, Issue Reporting guidelines, Development Setup and project structure are described. Through reading these contents, we can understand how to start the project, how to develop and how to explain the catalogue. The following is a brief introduction to important catalogues and documents, which you can read for yourself:

- build - --------------------------------------------------------------------------------------------------------------------------------------------------------
        -dist----------------------------------------------------------------------------------------------------------------------------------------------------
        - examples - ----------------------------------------------------------------------------------------------------------------------------------------------------
        - flow - ------------------------------------------------------------------------------- Type declaration, using open source project [Flow](https://flowtype.org/)
        -package.json------------------------------------------------------------------- No explanation
        - test - ------------------------------------------------------------------------------------------------------------------------------------------------
        -src - -------------------------------------------------------------------------------------------------------------------------- This is the directory that we should pay attention to most, including source code.
        - entries - --------------------------------------------------------------------------------------------------------------------------------------------------
        -web-runtime.js-------------------------------------------------------------------------- Runtime build entry, output dist/vue.common.js file, does not contain template to render function compiler, so it does not support the `template'option, we use Vue default export is the runtime version. When you use it, you should pay attention to it.
        - web-runtime-with-compiler.js -- an entry to a stand-alone build version that outputs dist/vue.js, which contains a compiler from template to render function
        - web-compiler.js--------------------- vue-template-compiler package entry file
        - web-server-renderer.js - ------- entry file of vue-server-renderer package
        - compiler------------------------------------------------------------------------------------------------------------------------------------------------------------------
        _ - parser - ------------------------------------------------------------------------------------------------------------------------------------------------
        Codgen - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------
        optimizer.js----------------------------------------------------------------------------------------------------------------------------------------------------------------
        Kernel - ------------------------------------------------------------------------------------------------------------------------------------------------------------
        _ - observer - ----------------------- reaction system, including the core code of data observation
        _ - vdom - ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        _ - instance - --------------------------------------------------------------------------------------------------------------------------------
        - global-api - ----------------------------------------------------------------------------------------------------------------------------------------------------------
        - components - ----------------------------------------------------------------------------------------------------------------------------
        _ - server - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------
        Platform - ----------------------------------------------------------------------------------------------------------------------------------------------------
        - sfc - ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
        _ - shared - ------------------------------------------------------------------------------------------------------------------------------------------
        

After we have a general understanding of the important directories and documents, we can check them. Development Setup In the Common Commands section, we will learn how to start this project. We can see the introduction as follows:

# watch and auto re-build dist/vue.js
    $ npm run dev
    # watch and auto re-run unit tests in Chrome
    $ npm run dev:test
    

Now, we just need to run npm run dev to monitor file changes and automatically rebuild the output dist/vue.js, and then run npm run dev:test to test. However, for convenience, I will create a new example in the examples directory, and then quote dist/vue.js so that we can take this example and change the Vue source code to see how we want to play.

2. Small Tips for Seeing Source Code

Before we really enter the source world, I would like to briefly talk about the techniques of looking at the source code:

Focus on the general framework, from macro to micro

When you look at a project code, it's best to find a main line. First, get the general process structure clear, then go deep into the details, break it one by one. Take Vue and raise a chestnut. If you already know that the data status in Vue changes, you will update the DOM in the way of virtual DOM. At this time, if you don't understand virtual DOM. So listen to my sentence, "Don't study internal implementation for the time being, because it will be your loss of the mainline," and you just need to know that virtual DOM is divided into three steps:

1. createElement(): Describe real DOM objects (real trees) with JavaScript objects (virtual trees). 2. Diff (old Node, new Node). Compare the differences between the old and new virtual trees, collect differences. 3. patch(): Apply differences to real DOM trees.

Sometimes the second step may be merged with the third step (like the patch in Vue). Besides, for example, the code in src/compiler/codegen, you may not know what he wrote, but it will be painful to see it directly, but you just need to know that CodeGen is used to generate Ren from the abstract syntax tree (AST). The der function is OK, which is to generate code similar to the following:

function anonymous() {
       with(this){return _c('p',{attrs:{"id":"app"}},[_v("\n      "+_s(a)+"\n      "),_c('my-com')])}
       }
    

When we know that something exists and its purpose, we can easily grasp the main line. The first article in this series is about the general main line. After understanding the general, we know what each part of the content is doing. For example, CodeGen generates functions similar to those shown in the code posted above. Then when we look at the code under codegen, the purpose will be stronger and it will be easier to understand.

3. What is the constructor of Vue?

There's a lot of balabala. Let's get started. The first thing we need to do is figure out what the Vue constructor looks like.

We know that we need to use the new operator to call the Vue, so that is to say, the Vue should be a constructor, so the first thing we need to do is to make the constructor clear first, how to find the Vue constructor? Of course, it starts with entry. Remember that when we run the npm run dev command, we will output dist/vue.js. So let's see what npm run dev did:

"dev": "TARGET=web-full-dev rollup -w -c build/config.js",
    

First set the TARGET value to `web-full-dev', then, if you don't know rollup, you should simply look at it... Simply put, it's a JavaScript module wrapper. You can simply interpret it as the same as webpack, but it has its advantages, such as Tree-shaking (webpack2 also has), but it also has its disadvantages in some scenarios... Not much nonsense, where - w is watch and - c is to specify the configuration file as build/config.js. Let's open this configuration file and see:

// Introducing dependencies, defining banner
    ...
    // builds object
    const builds = {
        ...
     // Runtime+compiler development build (Browser)
     'web-full-dev': {
           entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
          dest: path.resolve(__dirname, '../dist/vue.js'),
            format: 'umd',
          env: 'development',
         alias: { he: './entity-decoder' },
          banner
      },
      ...
      }
    // Method of Generating Configuration
    function genConfig(opts){
     ...
     }
    if (process.env.TARGET) {
      module.exports = genConfig(builds[process.env.TARGET])
      } else {
      exports.getBuild = name => genConfig(builds[name])
      exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
      }
    

The above code is simplified. When we run npm run dev, the value of process.env.TARGET is equal to'web-full-dev', so

module.exports = genConfig(builds[process.env.TARGET])
    

This code is equivalent to:

module.exports = genConfig({
        entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
        dest: path.resolve(__dirname, '../dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
        })
    

Finally, the genConfig function returns a config object, which is Rollup's configuration object. Then it's not hard to see that the entry file is:

src/entries/web-runtime-with-compiler.js
    

Let's open this file, don't forget our topic, we're looking for the Vue constructor, so when we see the first line of code for this file is:

import Vue from './web-runtime'
    

At this point, you should know that this file is temporarily out of touch with you, you should open the web-runtime.js file, but when you open this file, you find that the first line is as follows:

import Vue from 'core/index'
    

According to this idea, we finally find that the location of the Vue constructor should be in the src/core/instance/index.js file. In fact, we guess we can also guess that, when introducing the directory above, we said: instance is the directory where the Vue constructor design related code is stored. To sum up, the process we are looking for is as follows:

Let's look back at the src/core/instance/index.js file. It's very simple:

import { initMixin } from './init'
    import { stateMixin } from './state'
    import { renderMixin } from './render'
    import { eventsMixin } from './events'
    import { lifecycleMixin } from './lifecycle'
    import { warn } from '../util/index'
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
      }
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)
    export default Vue
    

By introducing dependencies, the Vue constructor is defined, and then five methods are invoked with the Vue constructor as a parameter. Finally, the Vue is derived. The five methods come from five files: init.js, state.js, render.js, events.js, and lifecycle.js.

Open these five files and find the corresponding methods. You will find that the function of these methods is to mount methods or attributes on the prototype of Vue. After these five methods, Vue will become like this:

// initMixin(Vue)   src/core/instance/init.js **************************************************
    Vue.prototype._init = function (options?: Object) {}
    // stateMixin(Vue)  src/core/instance/state.js **************************************************
    Vue.prototype.$data
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    Vue.prototype.$watch = function(){}
    // renderMixin(Vue)   src/core/instance/render.js **************************************************
    Vue.prototype.$nextTick = function (fn: Function) {}
    Vue.prototype._render = function (): VNode {}
    Vue.prototype._s = _toString
    Vue.prototype._v = createTextVNode
    Vue.prototype._n = toNumber
    Vue.prototype._e = createEmptyVNode
    Vue.prototype._q = looseEqual
    Vue.prototype._i = looseIndexOf
    Vue.prototype._m = function(){}
    Vue.prototype._o = function(){}
    Vue.prototype._f = function resolveFilter (id) {}
    Vue.prototype._l = function(){}
    Vue.prototype._t = function(){}
    Vue.prototype._b = function(){}
    Vue.prototype._k = function(){}
    // eventsMixin(Vue)   src/core/instance/events.js **************************************************
    Vue.prototype.$on = function (event: string, fn: Function): Component {}
    Vue.prototype.$once = function (event: string, fn: Function): Component {}
    Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
    Vue.prototype.$emit = function (event: string): Component {}
    // lifecycleMixin(Vue)   src/core/instance/lifecycle.js **************************************************
    Vue.prototype._mount = function(){}
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
    Vue.prototype._updateFromParent = function(){}
    Vue.prototype.$forceUpdate = function () {}
    Vue.prototype.$destroy = function () {}
    

Is that over? No, according to our previous search for the Vue route, this is just the beginning, we go back to the route, then the next processing Vue constructor should be the src/core/index.js file, we open it:

import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    initGlobalAPI(Vue)
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
      })
    Vue.version = '__VERSION__'
    export default Vue
    

This file is also very simple. It imports the Vue that has mounted methods and attributes on the prototype from instance/index, then imports initGlobal API and isServer Rendering, then passes Vue as a parameter to initGlobal API, and finally mounts $isServer on Vue.prototype, and ververs on Vue. Ion attributes.

The function of initGlobal API is to mount static attributes and methods on the Vue constructor. After initGlobal API, Vue will become as follows:

// src/core/index.js / src/core/global-api/index.js
    Vue.config
    Vue.util = util
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = util.nextTick
    Vue.options = {
        components: {
            KeepAlive
        },
        directives: {},
        filters: {},
        _base: Vue
        }
    Vue.use
    Vue.mixin
    Vue.cid = 0
    Vue.extend
    Vue.component = function(){}
    Vue.directive = function(){}
    Vue.filter = function(){}
    Vue.prototype.$isServer
    Vue.version = '__VERSION__'
    

One of the slightly more complicated ones is Vue.options, and you can tell by a little analysis that he really grew up like that. Next is the web-runtime.js file. The web-runtime.js file mainly does three things:

1. Override the attributes of Vue.config and set them as platform-specific methods 2, Vue.options.directives and Vue.options.components, install platform-specific instructions and components 3, define _patch_ and $mount on Vue.prototype

After the web-runtime.js file, the Vue becomes as follows:

// Install platform-specific utils
    Vue.config.isUnknownElement = isUnknownElement
    Vue.config.isReservedTag = isReservedTag
    Vue.config.getTagNamespace = getTagNamespace
    Vue.config.mustUseProp = mustUseProp
    // Install platform-specific instructions and components
    Vue.options = {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
        }
    Vue.prototype.__patch__
    Vue.prototype.$mount
    

What you should pay attention to here is the change of Vue.options. In addition, the $mount method here is simple:

Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      el = el && inBrowser ? query(el) : undefined
      return this._mount(el, hydrating)
      }
    

First, the query(el) element is acquired based on whether it is the browser environment, and then the EL is passed as a parameter to this._mount().

The last file that handles Vue is the entry file web-runtime-with-compiler.js, which does two things:

1. Caching the $mount function from the web-runtime.js file

const mount = Vue.prototype.$mount
    

Then the coverage covers Vue.prototype.$mount

2. Mount compile on Vue

Vue.compile = compileToFunctions
    

The function of compileToFunctions is to compile template into render function.

So far, we have restored the Vue constructor, to sum up:

1. The mounting of attributes and methods under Vue.prototype is mainly handled by the code in the src/core/instance directory.

2. The mounting of static attributes and methods under Vue is mainly handled by code under src/core/global-api directory

3. web-runtime.js mainly adds configuration, components and instructions specific to web platform. web-runtime-with-compiler.js adds compiler compiler to Vue's $mount method to support template.

IV. A Permanent Example

After understanding the design of the Vue constructor, we are going to present a throughout example. Applause is welcome:

let v = new Vue({
        el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
        })
    

Well, I admit that your children who have not had a full moon will write this code. This code is our example throughout the article. It is the main line of this article. In the follow-up explanations, we will take this code as an example. When we talk about the necessary, we will add options for it, such as calculating attributes, of course, we will add a computed attribute. But in the beginning, I just passed two options, el and data: "Let's see what happens next, and let's wait and see" - NBA stars like to say that in interviews.

What does Vue do when we code Vue as an example?

To know what Vue has done, we need to find the Vue initializer and look at the Vue constructor:

function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
      }
    

We find that the _init() method is the first method that Vue calls, and then passes our parameters options through. Before calling _init(), a security mode is also handled, telling the developer that the Vue must be invoked using the new operator. According to our previous arrangement, the _init() method should be defined in the src/core/instance/init.js file. We open this file to see the _init() method:

Vue.prototype._init = function (options?: Object) {
          const vm: Component = this
          // a uid
          vm._uid = uid++
          // a flag to avoid this being observed
          vm._isVue = true
          // merge options
          if (options && options._isComponent) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            initInternalComponent(vm, options)
          } else {
            vm.$options = mergeOptions(
              resolveConstructorOptions(vm.constructor),
              options || {},
              vm
            )
          }
          /* istanbul ignore else */
          if (process.env.NODE_ENV !== 'production') {
            initProxy(vm)
          } else {
            vm._renderProxy = vm
          }
          // expose real self
          vm._self = vm
          initLifecycle(vm)
          initEvents(vm)
          callHook(vm, 'beforeCreate')
          initState(vm)
          callHook(vm, 'created')
          initRender(vm)
          }
        

_ Initially, the init() method defines two attributes on this object: uid and isVue, and then decides if options._isComponent is defined. When using Vue to develop a project, we will not use the _isComponent option, which is used internally in Vue, at the beginning of this section. For example, we will go to the else branch, which is the code:

vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
      )
    

So the first step for Vue is to merge parameter options using policy objects

It can be found that Vue uses mergeOptions to handle the parameter options we pass in when we call Vue, and then assigns the return value to this.$options (vm == this) and passes three parameters to the mergeOptions method. Let's look at them separately. First, resolveConstructor Options (vm. constructor), let's look at them. Here's how to do this:

export function resolveConstructorOptions (Ctor: Class<Component>) {
      let options = Ctor.options
      if (Ctor.super) {
        const superOptions = Ctor.super.options
        const cachedSuperOptions = Ctor.superOptions
        const extendOptions = Ctor.extendOptions
        if (superOptions !== cachedSuperOptions) {
          // super option changed
          Ctor.superOptions = superOptions
          extendOptions.render = options.render
          extendOptions.staticRenderFns = options.staticRenderFns
          extendOptions._scopeId = options._scopeId
          options = Ctor.options = mergeOptions(superOptions, extendOptions)
          if (options.name) {
            options.components[options.name] = Ctor
          }
        }
      }
      return options
      }
    

This method receives a parameter, Ctor, which we can know by passing in the vm.constructor, is actually the Vue constructor itself. So the following code:

let options = Ctor.options
    

Amount to:

let options = Vue.options
    

Do you remember Vue.options? In the section on finding Vue constructors, we sorted out that Vue.options should look like the following:

Vue.options = {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
        }
    

We then determine whether we have defined Vue.super, which is used to handle inheritance. In this case, the resolveConstructorOptions method returns directly to Vue.options. That is to say, the first parameter passed to the mergeOptions method is Vue.options.

The second parameter passed to the mergeOptions method is the parameter option when we call the Vue constructor. The third parameter is the vm, that is, the object. Using the Vue as shown in the example at the beginning of this section, the final code should be as follows:

 vm.$options = mergeOptions(
         // Vue.options
       {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
        },
    // The parameter options passed in when calling the Vue constructor
       {
          el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
       },
       // this
       vm
     )
     

With this in mind, we can see what mergeOptions has done, and find out by reference that mergeOptions should be defined in the src/core/util/options.js file. The first time you look at this document, you may have a big head. Here is a brief presentation of my treatment. It should be easier for you to understand.

// 1. Reference dependency
    import Vue from '../instance/index'
    //Other references to ____________.
    // 2. Merge the policy object whose parent-child option value is the final value, when strategy is an empty object, because config.optionMergeStrategies = Object.create(null)
    const strats = config.optionMergeStrategies
    // 3. Define the same method on the strategy object as the name of the parameter option
    strats.el = 
    strats.propsData = function (parent, child, vm, key){}
    strats.data = function (parentVal, childVal, vm)
    config._lifecycleHooks.forEach(hook => {
      strats[hook] = mergeHook
      })
    config._assetTypes.forEach(function (type) {
      strats[type + 's'] = mergeAssets
      })
    strats.watch = function (parentVal, childVal)
    strats.props =
    strats.methods =
    strats.computed = function (parentVal: ?Object, childVal: ?Object)
    // The default merge policy returns `parentVal'if there is `child Val' and `parentVal'if there is no `child Val'.`
    const defaultStrat = function (parentVal: any, childVal: any): any {
      return childVal === undefined
        ? parentVal
        : childVal
        }
    // 4. Merge Options calls policy methods of the same name according to parameter options for merge processing
    export function mergeOptions (
      parent: Object,
      child: Object,
      vm?: Component
      ): Object {
      // Other code
      ...
      const options = {}
      let key
      for (key in parent) {
        mergeField(key)
      }
      for (key in child) {
        if (!hasOwn(parent, key)) {
          mergeField(key)
        }
      }
      function mergeField (key) {
        const strat = strats[key] || defaultStrat
        options[key] = strat(parent[key], child[key], vm, key)
      }
      return options
        
      }
    

In the above code, I omitted some tool functions, such as mergeHook and mergeAssets, etc. The only thing to note is this code:

config._lifecycleHooks.forEach(hook => {
      strats[hook] = mergeHook
      })
    config._assetTypes.forEach(function (type) {
      strats[type + 's'] = mergeAssets
      })
    

The config object refers to the src/core/config.js file. The final result is that merge policy function is merge Hook with the corresponding life cycle options added under strats, and merge policy function with the options of directives, components and filters is merge Assets.

This makes it much clearer. Take our example throughout this article.

let v = new Vue({
       el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
        })
    

The el option uses defaultStrat default policy function to process, the data option uses strats.data policy function to process, and according to the logic in strats.data, the strats.data method eventually returns a function: mergedInstanceDataFn.

Here we will not elaborate on the content of each policy function, and we will talk about it later. Here we still grasp the main line to sort out the main ideas. We only need to know that Vue uses a policy object to merge the father and son options when dealing with the options. And assign the final value to the $options attribute of the instance: this.$options, so let's continue to see what the _init() method does after merging the options:

After merging the options, the second part of Vue does: initialization and design of Vue instance objects

We talked about the design of Vue constructor, and sorted out the prototype attributes and methods of Vue and the static attributes and methods of Vue. The Vue instance object is created by the constructor. Let's see how the Vue instance object is designed. The following code is the generation after the _init() method merges the options. Code:

/* istanbul ignore else */
       if (process.env.NODE_ENV !== 'production') {
         initProxy(vm)
       } else {
         vm._renderProxy = vm
       }
       // expose real self
       vm._self = vm
       initLifecycle(vm)
       initEvents(vm)
       callHook(vm, 'beforeCreate')
       initState(vm)
       callHook(vm, 'created')
       initRender(vm)
       

According to the above code, two attributes are added to the instance in the production environment, and the attribute values are the instance itself:

vm._renderProxy = vm
    vm._self = vm
    

Then, four init* methods are called: initLifecycle, initEvents, initState, initRender, and life cycle hooks beforeCreate and create are called back before and after initState, respectively, while initRender is executed after the creation hook is executed, so you can see why C. DOM cannot be operated when created. Because no real DOM elements have been rendered into the document at this time. Created represents only the completion of initialization of the data state.

According to the reference relationship of the four init* methods, we open the corresponding files to see the corresponding methods. We find that these methods are dealing with Vue instance objects and doing some initialization work. Like sorting out the Vue constructor, I also sorted out the attributes and methods for the Vue instance, as follows:

// Attribute ************************************************************************************************************************ added to Vue.prototype.init
    this._uid = uid++
    this._isVue = true
    this.$options = {
        components,
        directives,
        filters,
        _base,
        el,
        data: mergedInstanceDataFn()
        }
    this._renderProxy = this
    this._self = this
    // Attribute ************************************************************************************************************************************** added to initLifecycle
    this.$parent = parent
    this.$root = parent ? parent.$root : this
     
    this.$children = []
    this.$refs = {}
    this._watcher = null
    this._inactive = false
    this._isMounted = false
    this._isDestroyed = false
    this._isBeingDestroyed = false
    // Attribute ****************************************************************************************************************************** added to initEvents
    this._events = {}
    this._updateListeners = function(){}
    // Attribute **************************************************************************************************************************** added to initState
    this._watchers = []
        // initData
        this._data
        // Attribute **************************************************************************************************************************** added to initRender
    this.$vnode = null // the placeholder node in parent tree
    this._vnode = null // the root of the child tree
    this._staticTrees = null
    this.$slots
    this.$scopedSlots
    this._c
    this.$createElement
    

These are the attributes and methods contained in a Vue instance. In addition to adding attributes in initEvents, if you have vm.$options._parentListeners, you also call the vm._updateListeners() method, and some other init methods are called in initState, as follows:

export function initState (vm: Component) {
      vm._watchers = []
      initProps(vm)
      initMethods(vm)
      initData(vm)
      initComputed(vm)
      initWatch(vm)
      }
    

Finally, if there is vm.$options.el in initRender, call vm.$mount(vm.$options.el), as follows:

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
      }
    

This is why Manual mount is required if the el option is not passed.

So let's take a look at what happened one by one, according to the examples at the beginning of this section and the order of initialization. Let's expand the init* method in initState and see that the order of execution should be this (top-down order):

initLifecycle(vm)
    initEvents(vm)
    callHook(vm, 'beforeCreate')
    initProps(vm)
    initMethods(vm)
    initData(vm)
    initComputed(vm)
    initWatch(vm)
    callHook(vm, 'created')
    initRender(vm)
    

First, initLifecycle, which adds attributes to the instance, and then initEvents, because the value of vm.$options._parentListeners is undefined, and only adding attributes to the instance, vm._updateListeners(listeners) will not execute because we only pass el. And data, so initProps, initMethods, initComputed, initWatch will do nothing, only initData will execute. Finally, initRender executes vm.$mount(vm.$options.el) because we pass in the El option, in addition to adding some attributes to the instance.

To sum up: As our example shows, initialization only includes two main elements: initData and initRender.

V. Viewing Vue's Data Response System through initData

The data response system of Vue includes three parts: Observer, Dep and Watcher. The content of the data response system has really been exhausted by the article, so I just want to say it briefly, in order to make everyone understand it, ok, let's take a look at the code in initData first.

function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? data.call(vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      let i = keys.length
      while (i--) {
        if (props && hasOwn(props, keys[i])) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${keys[i]}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else {
          proxy(vm, keys[i])
        }
      }
      // observe data
      observe(data)
      data.__ob__ && data.__ob__.vmCount++
      }
    

First, get the data data: let data = vm.$options.data. Do you remember that the value of vm.$options.data should be merged through the mergedInstanceDataFn function? So after getting the data, it determines whether the data type of the data is `function', and the final result is: data or the data of our incoming data option, that is:

data: {
        a: 1,
       b: [1, 2, 3]
       }
    

Then the _data attribute is defined on the instance object, which is the same reference as data.

Then there is a while loop. The purpose of the loop is to proxy the data on the instance object, so that we can access the data.a through this.a. The code is processed in the proxy function, which is very simple, just setting the accessor property with the same name as the data property on the instance object. Then use _data to do data hijacking, as follows:

function proxy (vm: Component, key: string) {
      if (!isReserved(key)) {
        Object.defineProperty(vm, key, {
          configurable: true,
          enumerable: true,
          get: function proxyGetter () {
            return vm._data[key]
          },
          set: function proxySetter (val) {
            vm._data[key] = val
          }
        })
      }
      }
    

After the data agent is finished, it formally enters the response system.

observe(data)
    

As we said, the data response system mainly consists of three parts: Observer, Dep and Watcher. The code is stored in the following files: observer/index.js, observer/dep.js and observer/watcher.js. This time, we will change the way. We don't look at the source code first, let's think about it first, and then go back. Looking at the code, you have a feeling of "Oh, but that's it".

Suppose we have the following code:

var data = {
        a: 1,
        b: {
            c: 2
        }
        }
    observer(data)
    new Watch('a', () => {
        alert(9)
        })
    new Watch('a', () => {
        alert(90)
        })
    new Watch('b.c', () => {
        alert(80)
        })
    

The purpose of this code is to first define a data object, then observe it through observer, and then define three observers. When the data changes, the corresponding method is executed. How does this function use Vue to realize? It's actually asking observer how to write? What about the Watch constructor? Next we will implement it one by one.

First of all, the role of observer is to convert the attributes of data object into accessor attributes:

class Observer {
        constructor (data) {
            this.walk(data)
        }
        walk (data) {
            // Traverse the data object properties and call the defineReactive method
            let keys = Object.keys(data)
            for(let i = 0; i < keys.length; i++){
                defineReactive(data, keys[i], data[keys[i]])
            }
        }
        }
    // The defineReactive method simply converts the properties of data into accessor properties
    function defineReactive (data, key, val) {
      // Recursive observation subattribute
        observer(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                return val
            },
            set: function (newVal) {
                if(val === newVal){
                    return
                }
                // Observation of new values
                observer(newVal)
            }
        })
        }
    // The observer method first determines whether data is a pure JavaScript object, and if so, calls the Observer class for observation.
    function observer (data) {
        if(Object.prototype.toString.call(data) !== '[object Object]') {
            return
        }
        new Observer(data)
        }
    

In the code above, we define the observer method, which detects whether data is a pure JavaScript object, and if so, calls the Observer class, passing data as a parameter. In Observer class, we use walk method to circularly call the defineReactive method on the attributes of data. The defineReactive method is very simple. It simply converts the attributes of data into accessor attributes and recursively observes the data. Otherwise, we can only observe the direct sub-attributes of data. This completes our first step, and when we modify or get the value of the data attribute, we can get the notification through get and set.

Let's go on and take a look at Watch:

new Watch('a', () => {
        alert(9)
        })
    

Now the question is, how does Watch relate to observer? Let's see what Watch knows and pass two parameters to Watch by calling Watch above. One is an expression which we can call'a'and the other is a callback function. So we can only write such code at present:

class Watch {
        constructor (exp, fn) {
            this.exp = exp
            this.fn = fn
        }
        }
    

So how do you relate it? Let's see what happens to the following code:

class Watch {
        constructor (exp, fn) {
            this.exp = exp
            this.fn = fn
            data[exp]
        }
        }
    

What is this sentence doing? Is it in getting the value of an attribute under data, such as exp for'a', that data[exp] is equivalent to getting the value of data.a, and what does that give rise to? Let's not forget that the attributes under data are accessor attributes at this time, so the result of doing so will trigger the get function of the corresponding attributes directly, so that we succeed in associating with observer, but that's not enough. We haven't achieved our goal yet, but we are infinitely close to it. Let's continue to think about the possibility of this:

Since evaluating expressions in Watch can trigger observer's get, can we collect functions in Watch in get?

The answer is yes, but then we need Dep. It's a dependent collector. Our idea is that every attribute under data has a unique Dep object. We collect dependencies only for that attribute in get, and then trigger all collected dependencies in set method. This is done. Look at the following code:

class Dep {
        constructor () {
            this.subs = []
        }
        addSub () {
            this.subs.push(Dep.target)
        }
        notify () {
            for(let i = 0; i < this.subs.length; i++){
                this.subs[i].fn()
            }
        }
        }
    Dep.target = null
    function pushTarget(watch){
        Dep.target = watch
        }
    class Watch {
        constructor (exp, fn) {
            this.exp = exp
            this.fn = fn
            pushTarget(this)
            data[exp]
        }
        }
    

In the above code, we added pushTarget(this) to Watch, and you can see that the purpose of this code is to set the value of Dep.target to the Watch object. After pushTarget, we evaluate the expression, and then we modify the defineReactive code as follows

function defineReactive (data, key, val) {
        observer(val)
        let dep = new Dep()       // Newly added
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                dep.addSub()   // Newly added
                return val
            },
            set: function (newVal) {
                if(val === newVal){
                    return
                }
                observer(newVal)
                dep.notify() // Newly added
            }
        })
        }
    

For example, we add three new sentences of code. We know that the expression evaluation in Watch triggers the get method. We call dep.addSub in the get method and execute the code: this.subs.push(Dep.target), because the value of Dep.target has been set to a Watch before the code is executed. Object, so the end result is to collect a Watch object, and then we call dep.notify in the set method, so when the value of the data attribute changes, we call the callback function in all collected Watch objects through the dep.notify loop:

notify () {
            for(let i = 0; i < this.subs.length; i++){
                this.subs[i].fn()
            }
            }
        

In this way, observer, Dep and Watch are linked to form an organic whole, which achieves our original goal. The complete code can be stamped here: observer-dep-watch . Here we dig a hole for you, because we did not deal with the observation of arrays, because it is more complex and this is not the focus of our discussion, if you want to know about this article that can poke me: When JavaScript implements MVVM, I just want to monitor the changes of a common object. In addition, when evaluating an expression in Watch, it only evaluates the direct sub-attributes, so if exp's value is'a.b', it can't be used. Vue's approach is to divide the expression string into arrays, and then traverse it to evaluate, you can see its source code. As follows:

/**
         * Parse simple path.
         */
         const bailRE = /[^\w.$]/
        export function parsePath (path: string): any {
          if (bailRE.test(path)) {
            return
          } else {
            const segments = path.split('.')
            return function (obj) {
              for (let i = 0; i < segments.length; i++) {
                if (!obj) return
                obj = obj[segments[i]]
              }
              return obj
            }
          }
          }
        

The evaluation code of Vue is implemented in parsePath function in src/core/util/lang.js file. To summarize the dependency collection process of Vue, it should be as follows:

In fact, Vue does not call addSub directly in get, but dep.depend. The purpose is to collect the current dep object into the watch object. If you want to complete the process, it should be like this: (Every field of data has its own dep object and get method.)

In this way, Vue builds up a data response system. As we said before, according to our example, initialization only includes two main contents: initData and initRender. Now that we've analyzed initData, let's take a look at initRender

6. Viewing render and re-render of Vue through initRender

In the initRender method, because we passed the el option in our example, the following code executes:

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
      }
    

Here, we call the $mount method, and when we restore the Vue constructor, we have sorted out all the methods, where the $mount method appears in two places:

1. In the web-runtime.js file:

Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      el = el && inBrowser ? query(el) : undefined
      return this._mount(el, hydrating)
      }
    

Its purpose is to get the corresponding DOM element by el, and then call the _mount method in the lifecycle.js file.

2. In the web-runtime-with-compiler.js file:

// Cached the $mount method from web-runtime.js
    const mount = Vue.prototype.$mount
    // Rewrite the $mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      // Obtain corresponding DOM elements according to el
      el = el && query(el)
      // You are not allowed to mount el to html tags or body Tags
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
      const options = this.$options
      // If we don't have the render option, try converting template or el into render functions
      if (!options.render) {
        let template = options.template
        if (template) {
          if (typeof template === 'string') {
            if (template.charAt(0) === '#') {
              template = idToTemplate(template)
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !template) {
                warn(
                  `Template element not found or is empty: ${options.template}`,
                  this
                )
              }
            }
          } else if (template.nodeType) {
            template = template.innerHTML
          } else {
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          template = getOuterHTML(el)
        }
        if (template) {
          const { render, staticRenderFns } = compileToFunctions(template, {
            warn,
            shouldDecodeNewlines,
            delimiters: options.delimiters
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
        }
      }
      // Call the $mount method in the cached web-runtime.js file
      return mount.call(this, el, hydrating)
      }
    

The logic of web-runtime-with-compiler.js is as follows:

1. Caching the $mount method from the web-runtime.js file

2. Determine if there is a render option passed, and if there is a direct call to the $mount method from the web-runtime.js file

3. If no render option is passed, check to see if there is a template option, and if so, compile it into a render function based on its content using the compileToFunctions function.

4. If there is no template option, check to see if there is an EL option. If there is one, compileToFunctions function is used to compile its content (template = getOuterHTML(el)) into render function.

5. Mount the compiled render function under this.$options property and call the $mount method in the cached web-runtime.js file

Simply use a graph to show the call relationship of mount method, call from top to bottom:

However, we find that the ultimate goal of these steps is to generate render functions and then call the _mount method in the lifecycle.js file. Let's see what this method does and see the code of the _mount method. This is a simplification:

Vue.prototype._mount = function (
      el?: Element | void,
      hydrating?: boolean
      ): Component {
      const vm: Component = this
      // Add the $el attribute to the Vue instance object to point to the mount point element
      vm.$el = el
      // Triggering the beforeMount life cycle hook
      callHook(vm, 'beforeMount')
      vm._watcher = new Watcher(vm, () => {
        vm._update(vm._render(), hydrating)
      }, noop)
      // If it's the first mount, it triggers the mounted lifecycle hook
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
      }
    

The above code is very simple. The annotations are all annotated. The only thing you need to see is this code:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
      }, noop)
    

Does it look familiar? We usually use Vue in this way:

this.$watch('a', (newVal, oldVal) => {
     
    })
    // perhaps
    this.$watch(function(){
      return this.a + this.b
      }, (newVal, oldVal) => {
       
    })
    

The first parameter is an expression or function, the second parameter is a callback function, and the third parameter is an optional option. The principle is that Watch internally evaluates expressions or functions to trigger data collection dependencies by the get method. But what is the first parameter vm when Watcher is used in the _mount method? Let's take a look at how the $watch function is implemented in the source code. According to what was sorted out in the previous restore Vue constructor, $warch method is defined in the stateMixin method in the src/core/instance/state.js file. The source code is as follows:

Vue.prototype.$watch = function (
      expOrFn: string | Function,
      cb: Function,
      options?: Object
      ): Function {
      const vm: Component = this
      options = options || {}
      options.user = true
      const watcher = new Watcher(vm, expOrFn, cb, options)
      if (options.immediate) {
        cb.call(vm, watcher.value)
      }
      return function unwatchFn () {
        watcher.teardown()
      }
      }
    

We can see that $warch is actually an encapsulation of Watcher, and the first parameter of the internal Watcher is actually vm: Vue instance object, which we can verify in Watcher's source code, instead of viewer/watcher.js file view:

export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: Object = {}
      ) {
        
      }
      }
    

It can be found that the real Watcher's first parameter is actually vm. The second parameter is an expression or function, and so on, so let's look at the code in _mount:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
      }, noop)
    

Ignoring the first parameter vm means that Watcher should evaluate the second parameter internally, that is, to run the function:

() => {
      vm._update(vm._render(), hydrating)
      }
    

So the vm._render() function is executed first. In src/core/instance/render.js, the method has a lot of code. The following is a simplified one:

Vue.prototype._render = function (): VNode {
      const vm: Component = this
      // Deconstruct render function in $options
      const {
        render,
        staticRenderFns,
        _parentVnode
      } = vm.$options
      ...
      let vnode
      try {
        // Running render function
        vnode = render.call(vm._renderProxy, vm.$createElement)
      } catch (e) {
        ...
      }
      
      // set parent
      vnode.parent = _parentVnode
      return vnode
      }
    

_ The render method first deconstructs the render function from vm.$options. You should remember that the vm.$options.render method is compiled from the web-runtime-with-compiler.js file using the compileToFunctions method to compile template or el. After deconstructing the render function, the method is then executed:

vnode = render.call(vm._renderProxy, vm.$createElement)
    

It uses call to specify the scope environment of render function as vm._renderProxy, which is added in the method of Vue.prototype._init, that is, vm._renderProxy = vm, which is actually the Vue instance object itself, and then passes a parameter: vm. $c. Create Element. So what exactly does render function do? Let's guess from the code above, we already know that render functions are compiled from template or el, and if they are correct, they should return a virtual DOM object. We might as well use console.log to print out render function when our template is written like this:

<ul id="app">
      <li>{{a}}</li>
      </ul>
    

Printed render functions are as follows:

We modify the template as follows:

<ul id="app">
      <li v-for="i in b">{{a}}</li>
      </ul>
    

Printed render functions are as follows:

In fact, the students who know Vue2.x version all know that Vue provides render option as a substitute for template and provides JavaScript with the ability of full programming. The following two ways of writing templates are actually equivalent:

// Programme 1:
    new Vue({
     el: '#app',
     data: {
         a: 1
        },
      template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>'
      })
    // Programme II:
    new Vue({
     el: '#app',
     render: function (createElement) {
          createElement('ul', [
               createElement('li', this.a),
                createElement('li', this.a)
         ])
      }
      })
    

Now let's look at the render function we printed:

function anonymous() {
     with(this){
         return _c('ul', { 
              attrs: {"id": "app"}
            },[
             _c('li', [_v(_s(a))])
           ])
      }
      }
    

Is it similar to the render function we write ourselves? Because the scope of the render function is bound to the Vue instance, that is, render.call(vm._renderProxy, vm.$createElement), the _c, _v, _s and variable a in the above code correspond to the methods and variables in the Vue instance. Do you remember where methods such as _c, _v, _s were defined? When we collate the Vue constructors, we know that they are defined in the renderMixin method in the src/core/instance/render.js file, besides these, there are also such things as _l,_m,_o, etc. Where _l appears when we use the v-for instruction. So now you know why these methods are defined in render. JS files, because they exist to construct render functions.

Now we know what the render function looks like and the scope of the render function is the Vue instance itself: this (or vm). So when we execute the render function, the variables in it, such as: a, are equivalent to: this.a. We know that this is the evaluation, so the code in _mount:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
      }, noop)
    

When vm._render is executed, the dependent variables are evaluated and collected as dependencies. According to the logic of watcher.js in Vue, when the dependent variables are changed, not only the callback function is executed, but actually the value is revalued, that is, it is executed once:

() => {
      vm._update(vm._render(), hydrating)
      }
    

This actually does re-render, because vm._update is the last step in the virtual DOM mentioned at the beginning of the article: patch

The vm_render method eventually returns a vnode object, virtual DOM, and passes the past as the first parameter of vm_update. Let's take a look at the logic of vm_update. There is such a code in the src/core/instance/lifecycle.js file:

if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
      }
    

If there is no prevVnode description, it is the first rendering, create the real DOM directly. If prevVnode already exists, it is not the first time to render, then the patch algorithm is used to perform the necessary DOM operations. This is the logic of Vue updating DOM. It's just that we haven't implemented virtual DOM internally.

Now let's straighten out our thinking when we write the following code:

new Vue({
        el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
        })
    

What Vue does:

1. Build a data response system, use Observer to convert data into accessor attributes, compile el into render function, render function return value is virtual DOM.

2. Evaluate _update in _mount, and _update evaluates render. Within render, it evaluates dependent variables and collects them as dependent variables to be evaluated. When the variables change, update will be re-executed again to achieve re-render.

This is the case with a more detailed picture:

So far, we have gone through the Vue from the general process, focusing on the key points, but there are many details we did not mention, such as:

1. When the template is converted to render function, it is actually Mr. AST, and then the abstract syntax tree into render function. And we have not mentioned this whole set of code, because he is complex, in fact, this part of the content is regular.

2. We haven't talked about the principle of Virtual DOM in detail. We have already talked about it on the internet. You can search for it.

3. In our example, we only pass in the el, data options. We know that Vue supports many options, such as we did not mention, but they are all by-the-way. For example, if you get the data options clear, it will be easy to look at the computed options or props options. For example, you know the work of Watcher. It's easy for the mechanism to look at the watch option again.

As an enlightening article of Vue source code, this article may have many shortcomings, which should be a valuable reference.

Topics: Vue Attribute npm Javascript