.1-Vue源码起步

搞事!搞事!

 

  截止 2017.5.16,终于把 vue 的源码全部抄完,总共有 9624 行,花时大概一个月时间,中间迭代了一个版本(2.2-2.3),部分代码可能不一致,不过没关系!
  上一个链接 https://github.com/pflhm2005/vue

 

  进入阶段 2:尝试一下,从小案例看一看代码在 vue 源码中的走向,Go!(语文不好,将就看看)

 

  从最简单的案例开始,摘抄官网的起步:

    <body>
        <div id='app'>
            {{message}}
        </div>
    </body>
    <script src='./vue.js'></script>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                message: 'Hello Vue!'
            }
        });
    </script>

  打断点,开始执行!

 

初始化函数

  html 代码中,包含 2 大部分,挂载 DOM 节点,以及初始化 vue 的 js 代码。

  

  有几个小地方,虽然按照官网的案例不会出现问题,但是还是说明一下:

  (1)、el 不能挂载到 html 或者 body 标签上

    // Line-9547
    if (el === document.body || el === document.documentElement) {
        "development" !== 'production' && warn(
            "Do not mount Vue to <html> or <body> - mount to normal elements instead."
        );
        return this
    }

   (2)、关于代码各处可见的 "development" !== 'production'     

  这个是 dev 模式才有,vue.js 文件中对所有警告的代码判断条件进行了替换,报错方便调试,在发布模式中会自动清除,方便开发。  

 

  与 jQuery 不一样,这里需要手动 new 才会创建一个 vue 实例。

  直接上源码。

 

  jQuery:

    // Line-94 jQuery 3.2.1
    // 顺便吐槽一下 这个版本终于把初始化提前了 代码结构应该棒棒的
    var jQuery = function(selector, context) {
        // The jQuery object is actually just the init constructor 'enhanced'
        // Need init if jQuery is called (just allow error to be thrown if not included)
        return new jQuery.fn.init(selector, context);
    }

  Vue:

    // Line-9622
    return Vue$3;

  

  但是我们看到源码最后其实返回的是 Vue$3,至于为什么 new 的是 Vue 也行呢?看一下源码开头的整个 IIFE 表达式也就明白了。

    (function(global, factory) {
        // 兼容各种宿主环境
        typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
            typeof define === 'function' && define.amd ? define(factory) :
            // 浏览器环境
            (global.Vue = factory());}(this, /*vue*/ ));

  基本上框架都是这个套路,引入一个宿主环境的对象以及框架本身。

  上述代码形参中,global 在浏览器环境中相当于 window,由于有时会在 node、webpack 等环境中运行,所以需要进行兼容处理,于是有很长的 typeof。

  对于浏览器来讲,上述代码其实就是 window.Vue = Vue$3({options}),所以这就很明了了。

  

  起步流程两个框架都是一样的,首先通过一个 init 函数进行全局初始化。

    // Line-4055
    function Vue$3(options) {
        if ("development" !== 'production' &&
            !(this instanceof Vue$3)) {
            warn('Vue is a constructor and should be called with the `new` keyword');}
        this._init(options);
    }

  这里的 options 参数,很明显就是我们在 new 对象的时候传进去的对象,目前只有 el 和 data 两个。

  入口函数只是简单的判断了一下有没有 new,然后自动调用了原型函数 _init。

 

  _init 函数的定义地点有点意思,是在一个函数内部定义,然后在后面调用了这个函数。

    // Line-3924
    function initMixin(Vue) {
        Vue.prototype._init = function(options) {
            //....
        };
    }
    // Line-4063
    initMixin(Vue$3);

  整个函数只定义了 _init 这个初始化原型函数,原因在某个注释中写,直接定义原型会出现问题,所以采用这种方法进行规避。

  至于具体什么问题,我找不到那行注释了。。。

 

  接下来看看初始化函数里面都干了啥事。

    // Line-3926
    // 生成的实例保存为别名 vm
    var vm = this;
    // 全局记数 表示有几个 vue 实例
    vm._uid = uid$1++;
    var startTag, endTag;
    // 这里的 config.performance 开发版默认是 false
    if ("development" !== 'production' && config.performance && mark) {
        startTag = "vue-perf-init:" + (vm._uid);
        endTag = "vue-perf-end:" + (vm._uid);
        mark(startTag);
    }
    // 代表这是一个 vue 实例
    vm._isVue = true;
    // 非组件 跳过
    if (options && options._isComponent) {initInternalComponent(vm, options);
    }
    // 正常实例初始化
    // 在这里对参数进行二次加工
    else {
        vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),
            options || {},
            vm
        );}
    // ...more

 

  前面基本上没做什么事,对于 config 对象,在开发版中的默认参数如下:

    // 开发版的默认配置
    var config = ({
        optionMergeStrategies: Object.create(null),
        silent: false,
        productionTip: "development" !== 'production',
        devtools: "development" !== 'production',
        performance: false,
        errorHandler: null,
        ignoredElements: [],
        keyCodes: Object.create(null),
        isReservedTag: no,
        isReservedAttr: no,
        isUnknownElement: no,
        getTagNamespace: noop,
        parsePlatformTagName: identity,
        mustUseProp: no,
        // 历史遗留
        _lifecycleHooks: LIFECYCLE_HOOKS
    });

  由于提示信息不是重点,所以第一步直接可以走到 mergeOptions 这里,从名字就可以看出这是一个参数合并的函数,接受 3 个参数:

 

  1、resolveConstructorOptions(vm.constructor) 

  这个函数属于内部初始化,接受的参数就是 Vue 函数自身,如下:

    // Line-4136
    Sub.prototype.constructor = Sub;

  跳进去看一眼这个函数做了什么:

    // Line-3998
    function resolveConstructorOptions(Ctor) {
        // Ctor=Constructor
        // options 为所有 vue 实例基础参数
        // 包含 components,directives,filters,_base
        var options = Ctor.options;
        // 这个属性比较麻烦 暂时没有 跳过
        if (Ctor.super) {
            //...
        }
        // 返回修正后的 options
        return options
    }

  如果忽略那个 super 属性的话,返回的其实就是 Vue$3.constructor.options,该对象包含 4 个属性,如图所示。

  

    // Line-4368
    // Vue 函数自身的引用
    Vue.options._base = Vue;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Line-7523</span>
extend(Vue$3<span style="color: rgba(0, 0, 0, 1)">.options.directives, platformDirectives);
extend(Vue$</span>3<span style="color: rgba(0, 0, 0, 1)">.options.components, platformComponents);

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Line-7161</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 指令相关方法</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> platformDirectives =<span style="color: rgba(0, 0, 0, 1)"> {
    model: model$</span>1<span style="color: rgba(0, 0, 0, 1)">,
    show: show
};<br>
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Line-7509</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 组件相关</span>
<span style="color: rgba(0, 0, 255, 1)">var</span> platformComponents =<span style="color: rgba(0, 0, 0, 1)"> {
    Transition: Transition,
    TransitionGroup: TransitionGroup
};</span></pre>

  其中 filters 属性暂时是空的,其余 3 个属性在 2 个地方有定义,一个是组件、指令方法集,一个是 vue 函数自身引用。

  2、options || {} => 传进来的参数

  3、vm => 当前 vue 实例

 

   最后,总览 3 个参数如下:

   带着 3 个小东西,跳进了 mergeOptions 函数进行参数合并。

    // Line-1298
    // 父子组件合并参数 本案例父组件为默认对象
    function mergeOptions(parent, child, vm) {
        // 检测 components 参数中键是否合法
        checkComponents(child);
        if (typeof child === 'function') {
            child = child.options;
        }
        // 格式化 props,directives 参数
        normalizeProps(child);
        normalizeDirectives(child);
        // 格式化 extends 参数
        var extendsFrom = child.extends;
        if (extendsFrom) {
            parent = mergeOptions(parent, extendsFrom, vm);
        }
        // mixins 参数
        if (child.mixins) {
            for (var i = 0, l = child.mixins.length; i < l; i++) {
                parent = mergeOptions(parent, child.mixins[i], vm);}
        }
        // 本案例中上面的都会跳过
        var options = {};
        var key;
        // 遍历父组件对象 合并键
        for (key in parent) {mergeField(key);
        }
        // 遍历子组件对象 若有父组件没有的 合并键
        for (key in child) {
            if (!hasOwn(parent, key)) {mergeField(key);
            }
        }
        // 合并函数
        function mergeField(key) {
            var strat = strats[key] || defaultStrat;
            options[key] = strat(parent[key], child[key], vm, key);}
        return options
    }

  这个函数中前半部分可以跳过,因为只有简单的 el、data 参数,所以直接从 mergeField 开始执行。

  上面已经列举出父组件的键,有 components、directives、_filters、_base 四个。

 

  这里又多出一个新的东西,叫 strats,英文翻译成战略,所以应该怎么叫我也是很懵逼的。这个对象内容十分丰富,从生命周期到 data、computed、methods 都有,如下所示:

   

  方法太多,就不一个一个讲了,说说本案例相关的几个方法。

  看起来非常吓人,其实定义简单粗暴,上代码看看就明白了。

    // Line-281
    var ASSET_TYPES = [
        'component',
        'directive',
        'filter'
    ];
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Line-1182</span>
ASSET_TYPES.forEach(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(type) {
    strats[type </span>+ 's'] =<span style="color: rgba(0, 0, 0, 1)"> mergeAssets;
});

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Line-1175</span>
<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> mergeAssets(parentVal, childVal) {
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> res = Object.create(parentVal || <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> childVal ?<span style="color: rgba(0, 0, 0, 1)">
        extend(res, childVal) :
        res
}</span></pre>

  简单讲就是,3 个键对应的是同一个方法,接受 2 个参数,方法还贼简单。

  所以,对上面的 mergeOptions 函数进行简化,可以转换成如下代码:

    // parent 键:components、directives、_filters、_base
    // child 键:data、el
    function mergeOptions(parent, child, vm) {
        var options = {};
        var key;
        // 父子对象键没有重复 参数直接可以写 undefined 一步一步简化
        for (key in parent) {
            //options[key] = mergeAssets(parent[key], child[key], vm, key);
            //options[key] = mergeAssets(parent[key], undefined);
            options[key] = Object.create(parent[key]);
        }
        // 子键 data 和 el 需要额外分析 第一个参数同样可以写成 undefined
        for (key in child) {
            if (!hasOwn(parent, key)) {
                //options[key] = strats[key](parent[key], child[key], vm, key);
                options[key] = strats[key](undefined, child[key], vm, key);}
        }
        return options
    }
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> mergeAssets(parentVal, childVal) {
    </span><span style="color: rgba(0, 0, 255, 1)">var</span> res = Object.create(parentVal || <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> childVal ?<span style="color: rgba(0, 0, 0, 1)">
        extend(res, childVal) :
        res
}</span></pre>

  遍历父对象其实啥也没做,直接把几个方法加到了 options 上面,然后开始遍历子对象,子对象包含我们传进去的 el、data。

  el 比较简单,只是做个判断然后丢回来。

    // Line-1064
    // 简单判断是否是 vue 实例挂载的 el
    strats.el = strats.propsData = function(parent, child, vm, key) {
        if (!vm) {
            warn(
                "option \""+ key +"\"can only be used during instance" +
                'creation with the `new` keyword.'
            );}
        return defaultStrat(parent, child)
    };

  data 则分两种情况,一种是未挂载的组件,一种是实例化的 vue。

  不管未挂载,直接看实例化 vue 是如何处理 data 参数。

    // Line-1098
    strats.data = function(parentVal, childVal, vm) {
        // 未挂载
        if (!vm) {
            //...
        }
        // new 出来的
        // 传进来的 parentVal、childVal 分别为 undefined、{message:'Hello Vue!} 
        else if (parentVal || childVal) {
            return function mergedInstanceDataFn() {
                var instanceData = typeof childVal === 'function' ?
                    childVal.call(vm) :
                    childVal;
                var defaultData = typeof parentVal === 'function' ?
                    parentVal.call(vm) :
                    undefined;
                if (instanceData) {
                    return mergeData(instanceData, defaultData)
                } else {
                    return defaultData
                }
            }
        }
    };

  这里直接返回了一个函数,暂时不做分析,后面执行时候再来看。

 

  到此,整个 mergeOptions 函数执行完毕,返回一个处理过的 options,将这个结果给了实例的 $options 属性:

 

  最后,用一张图结束这个乱糟糟的源码小跑第一节吧。

 

 

  撒花!撒花!