• .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;
    
        // Line-7523
        extend(Vue$3.options.directives, platformDirectives);
        extend(Vue$3.options.components, platformComponents);
    
        // Line-7161
        // 指令相关方法
        var platformDirectives = {
            model: model$1,
            show: show
        };
    // Line-7509 // 组件相关 var platformComponents = { Transition: Transition, TransitionGroup: TransitionGroup };

      其中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'
        ];
    
        // Line-1182
        ASSET_TYPES.forEach(function(type) {
            strats[type + 's'] = mergeAssets;
        });
    
        // Line-1175
        function mergeAssets(parentVal, childVal) {
            var res = Object.create(parentVal || null);
            return childVal ?
                extend(res, childVal) :
                res
        }

      简单讲就是,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
        }
    
        function mergeAssets(parentVal, childVal) {
            var res = Object.create(parentVal || null);
            return childVal ?
                extend(res, childVal) :
                res
        }

      遍历父对象其实啥也没做,直接把几个方法加到了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属性:

     

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

      撒花!撒花!

      

  • 相关阅读:
    【代码笔记】iOS-JQIndicatorViewDemo
    【代码笔记】iOS-plist获得城市列表
    【代码笔记】iOS-HTTPQueue下载图片
    【代码笔记】iOS-请求去掉url中的空格
    【代码笔记】iOS-collectionView实现照片删除
    【代码笔记】iOS-自定义loading(IanAlert)
    Foundation框架-NSCalendar
    iOS Xcode全面剖析
    Mac下Git的基础操作
    iOS开发UI篇—常见的项目文件介绍
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/6862539.html
Copyright © 2020-2023  润新知