初始化

上一章节,我们找到了 Vue 的入口文件,从这章节我们就从入口文件,开始了解 Vue 的初始化,真正的开始 Vue 源码的学习。 在使用 Vue 的时候,我们一般都是这么用的:

let v = new Vue({
    el: "#app",
    data: {
        a: 1,
        b: [1, 2, 3]
    }
});
1
2
3
4
5
6
7

开始学习源码,我们就要带着疑问学习。在上面我们 new 了一个 Vue 的实例,那么这个 Vue 的实例都做了些什么呢?我们就结合 Vue 的定义来看一下:

...
function Vue(options) {
    // options = {
        // el:"#app",
        // data:{
            // a:1,
            // b:[1,2,3]
        // }
    // }
    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);
}

export default Vue;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

从上面可以看出来,Vue 是一个构造函数,这个构造函数调用了_init()方法,那么我们就看一下这个方法src/core/instance/init.js

export function initMixin(Vue: Class<Component>) {
    // options==={el:'#app,data:{a:1,b:[1,2,3]}}
    Vue.prototype._init = function(options?: Object) {
        const vm: Component = this;
        // a uid
        vm._uid = uid++;

        let startTag, endTag;
        /* istanbul ignore if */
        if (
            process.env.NODE_ENV !== "production" &&
            config.performance &&
            mark
        ) {
            startTag = `vue-perf-start:${vm._uid}`;
            endTag = `vue-perf-end:${vm._uid}`;
            mark(startTag);
        }

        // 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); //初始化事件
        initRender(vm); //初始化渲染
        callHook(vm, "beforeCreate");
        initInjections(vm); // resolve injections before data/props
        initState(vm); //vm的状态初始化,prop/data/computed/method/watch都在这里完成初始化
        initProvide(vm); // resolve provide after data/props
        callHook(vm, "created");

        /* istanbul ignore if */
        if (
            process.env.NODE_ENV !== "production" &&
            config.performance &&
            mark
        ) {
            vm._name = formatComponentName(vm, false);
            mark(endTag);
            measure(`vue ${vm._name} init`, startTag, endTag);
        }

        if (vm.$options.el) {
            vm.$mount(vm.$options.el); //挂载元素
        }
    };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

_init()这个方法就是首先定义了几个常量,然后调用mergeOptions() 对传入的参数options进行了  合并,最后就是初始化了生命周期、事件、渲染以及 data/prop/method 等 。这个时候还调用了 Vue 的钩子函数。最后调用$mount进行了元素的挂载。
那我们就来看一下是如何合并 options 的,因为在mergeOptions()这个方法接收了resolveConstructorOptions(vm.constructor)这个方法的返回值,所有先看一下这个方法的返回值是什么src/core/instance/init.js:

export function resolveConstructorOptions(Ctor: Class<Component>) {
    // 在调用的时候是resolveConstructorOptions(vm.constructor)
    // vm.constructor就是Vue的方法
    //Vue.options = {
    //   components: {},
    //   directives: {},
    //   filters: {},
    //   _base: Vue
    // }

    let options = Ctor.options;
    if (Ctor.super) {
        // 有super属性,说明Ctor是Vue.extend构建的子类
        const superOptions = resolveConstructorOptions(Ctor.super);
        // Vue构造函数上的options,如directives,filters,....
        const cachedSuperOptions = Ctor.superOptions;
        if (superOptions !== cachedSuperOptions) {
            // super option changed,
            // need to resolve new options.
            Ctor.superOptions = superOptions;
            // check if there are any late-modified/attached options (#4976)
            const modifiedOptions = resolveModifiedOptions(Ctor);
            // update base extend options
            if (modifiedOptions) {
                extend(Ctor.extendOptions, modifiedOptions);
            }
            options = Ctor.options = mergeOptions(
                superOptions,
                Ctor.extendOptions
            );
            if (options.name) {
                options.components[options.name] = Ctor;
            }
        }
    }
    // 如果Ctor是基础Vue构造器时,就如我们通过new关键字 new Vue实例的时候,
    // options就是Vue构造函数上的options
    return options;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

resolveConstructorOptions方法中对传进来的参数进行了是否是 Vue 的基础构造器的判断。我们就先看一下如果是 Vue 基础构造器的时候,这个时候Ctor.options就是 Vue 构造函数上的 options。那么,Vue 构造函数上的 options 又是什么呢?到目前位置我们只分析了入口文件,和 Vue 定义之后只是调用了_init()方法,并没有看到 options 在哪里定义的,那么这个 options 到底是什么呢?
大家是否还记得,我们在分析入口文件的时候,找到了src/platforms/web/entry-runtime-with-compiler这个文件,这个文件呢就是对 Vue 进行了第一层的包装,但是并没有 options 的相关内容,那么我们往上一级寻找,找到了src/platforms/web/runtime/index.js这个文件,这是对 Vue 的第二层包装,在这里可以看到有关 options 的内容:

...
import Vue from "core/index"; //第三次包装
...
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
...
1
2
3
4
5
6

上面的代码主要是给 Vue.options 添加一些属性,并没有真正的定义。我们再次往上找,找到src/core/global-api/index.js:

...
Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  };

  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;

  Vue.options = Object.create(null);
  //ASSET_TYPES = [
  //   'component',
  //   'directive',
  //   'filter'
  // ]
  ASSET_TYPES.forEach(type => {
    Vue.options[type + "s"] = Object.create(null);
  });

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue;

  extend(Vue.options.components, builtInComponents);

  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

至此我们发现 Vue 构造函数上的 options 就是

Vue.options = {
    components: {},
    directives: {},
    filters: {},
    _base: Vue
};
1
2
3
4
5
6

如图

Vue构造函数的options

因此当 resolveConstructorOptions()方法就是在 Ctor.super 不存在时,返回了 Vue 构造函数的 otpions(Ctor.super 是通过 Vue.extend 构造子类的时候,Vue.extend 会为 Ctor 添加一个 super 属性,指向其父类构造器,这种情况比较复杂,先不讨论)。 我们再回来看一下vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm );此时就可以替换成vm.$options = mergeOptions( vm.options, options || {}, vm );,那么我们再来看一下mergeOptions()(src/core/util/options.js):

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions(
    parent: Object,
    child: Object,
    vm?: Component
): Object {
    // 传入的三个参数分别代表:
    // 1、Vue实例构造函数上的options
    // 2、实例化是我们自己传入的参数
    // 3、vm实例本身
    // 也就是这个方法就是合并parent和child
    //parent = {
    //   components: {},
    //   directives: {},
    //   filters: {},
    //   _base: Vue
    // }
    // child===={el:'#app,data:{a:1,b:[1,2,3]}}
    if (process.env.NODE_ENV !== "production") {
        checkComponents(child); //检查组件名称是否合法
    }

    if (typeof child === "function") {
        child = child.options; //如果child是function类型的话,我们取options属性作为child
    }

    normalizeProps(child, vm); //把props属性转换成对象的形式
    normalizeInject(child, vm); //把inject属性转换成对象的形式
    normalizeDirectives(child); //把directives属性转换成对象的形式

    const extendsFrom = child.extends;
    if (extendsFrom) {
        // 当传入的options里有extends时,再次调用mergeOptions方法进行合并
        parent = mergeOptions(parent, extendsFrom, vm);
    }
    if (child.mixins) {
        // 当传入的options里有mixin时,再次调用mergeOptions方法进行合并
        //
        for (let i = 0, l = child.mixins.length; i < l; i++) {
            parent = mergeOptions(parent, child.mixins[i], vm);
        }
    }
    const options = {};
    let key;
    for (key in parent) {
        mergeField(key);
    }
    for (key in child) {
        if (!hasOwn(parent, key)) {
            mergeField(key);
        }
    }
    function mergeField(key) {
        //const defaultStrat = function(parentVal: any, childVal: any): any {
        //   return childVal === undefined ? parentVal : childVal;
        // };
        // defaultStrat的逻辑就是,如果child上该属性值存在,就取child上的,如果不存在,就取parent上的

        //const strats = config.optionMergeStrategies;
        const strat = strats[key] || defaultStrat;
        options[key] = strat(parent[key], child[key], vm, key);
    }
    return options;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

我们先翻译一下mergeOptions()最开始的两行注释,就是:将两个options对象合并生成一个新的options。这个就是实例化和继承中使用的核心方法。 我们把mergeOptions()中的方法都放在了代码注释中,可以仔细看一下,有些内容还是没有细讲,以后会有专门的文章进行讲解。最后返回了合并后的 options。 借用淼淼真人的图来最后总结一下mergeOptions()mergeoptions

总结

Vue 的初始化文件init.js就是将 options 进行了合并,最后对生命周期、事件等进行了初始化,然后把这些进行了挂载。

支付宝
微信