vue源码分析【2】-new Vue的过程

607 阅读16分钟

当前篇:vue源码分析【2】-new Vue的过程

以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。

模板代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="./../../oldVue.js"></script>
</head>

<body>
    <div id="app">
        <h2>开始存钱</h2>
        <div>每月存 :¥{{ money }}</div>
        <div>存:{{ num }}个月</div>
        <div>总共存款: ¥{{ total }}</div>
        <button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
        <msg-tip :msginfo='msgText' :totalnum='total'></msg-tip>
    </div>

    <script>
        debugger;
        // 定义一个新组件
        var a =  {
            props:['msginfo', 'totalnum'],
            data: function () {
                return {
                    count: 0
                }
            },
            template: '<div>{{ msginfo }}存了¥{{ totalnum }}</div>'
        }

        var app = new Vue({
            el: '#app',
            components: { msgTip: a},
            beforeCreate() { },
            created() { },
            beforeMount() { },
            mounted: () => { },
            beforeUpdate() { },
            updated() { },
            beforeDestroy() { },
            destroyed() { },
            data: function () {
                return {
                    money: 100,
                    num: 12,
                    arryList: [{name:'子树'}],
                    msgText: "优秀的乃古:"
                }
            },
            computed: {
                total() {
                    return this.money * this.num;
                }
            },
            methods: {
                getMoreMoney() {
                    this.money = this.money * 2
                    this.arryList.unshift({name: '大树'})
                }
            }
        })

    </script>

</body>

</html>

1. 前言

本文的结构依据点,线,面来展开。

  • 点即函数的作用
  • 线即函数的执行流程
  • 面即源码的详细解读

十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。


2. 整体流程

3. 触发new Vue

在html的js中执行new Vue,此时会进入vue.js中的Vue构造函数中。

options参数是html中new Vue的入参。

源码:

// html中触发
var app = new Vue({...})

// vue.js中的Vue构造函数
function Vue(options) {
        // 在vue的webpack版本中这里是:development是process.env.NODE_ENV
        if ("development" !== 'production' && !(this instanceof Vue)
        ) {
            warn('Vue is a constructor and should be called with the `new` keyword');
        }
        this._init(options);
}

我们先看下options的入参是什么:

{
    beforeCreate: ƒ beforeCreate(),
    beforeDestroy: ƒ beforeDestroy(),
    beforeMount: ƒ beforeMount(),
    beforeUpdate: ƒ beforeUpdate(),
    components: {  // 组件
        msgTip: { // 组件名
            data: ƒ (),
            props: [
                msginfo,
                totalnum
            ],
            template: "<div>{{ msginfo }}存了¥{{ totalnum }}</div>"
        }
   },
    computed: {total: ƒ}
    created: ƒ created()
    data: ƒ ()
    destroyed: ƒ destroyed()
    el: "#app"
    methods: {getMoreMoney: ƒ}
    mounted: () => { }
    updated: ƒ updated()
    __proto__: Object
}

4. this._init

作用:

  1. 初始化 vm.$options(合并选项)
  2. 一堆初始化的工作(包括初始化生命周期、事件、渲染函数、data、prop、methods、Computed、watch 等等)
  3. mount 生命钩子挂载元素(完整版构建就会有 Compiler 编译过程来生成渲染函数)

源码:

// 它执行的时候,会走Vue.prototype._init
this._init(options);

var uid$3 = 0;

//初始化vue
function initMixin(Vue) {
        Vue.prototype._init = function (options) {
            var vm = this;
            // 递增给每一个组件加一个唯一的uid
            // uid$3本身递增了1,但是表达式没有增加,所以vm._uid初始化为0
            vm._uid = uid$3++;
            
            //开始,结束标签
            var startTag, endTag;
            // 浏览器性能监控
            // 【说明1 config】
            if ("development" !== 'production' && config.performance && mark) {
                startTag = "vue-perf-start:" + (vm._uid);
                endTag = "vue-perf-end:" + (vm._uid);
                mark(startTag);
            }
            // 一个避免被观察到的标志
            vm._isVue = true;
            // 如果当前文件是个组件,执行子组件初始化
            if (options && options._isComponent) { 
            // 将new Vue入参options的属性放到 vm.$options 中
                initInternalComponent(vm, options);
            } else {
                /**
                * 这一块都是围绕合并options,然后返回合并后的options
                *
                * 初始化根组件时走这里,合并 Vue 的全局配置到根组件的局部配置,比如 
                * Vue.component 注册的全局组件会合并到 根实例的 components 选项中
                */
                //合并参数 将父值对象和子值对象合并在一起
                vm.$options = mergeOptions(
                    // 解析constructor上的options属性的
                    resolveConstructorOptions(vm.constructor), 
                    options || {},
                    vm 
                );
            }
            {
                //初始化 代理 监听
                initProxy(vm);
            }
            // 执行一系列钩子函数等
            vm._self = vm;
            // 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
            initLifecycle(vm);
              /**
             * 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上
             * 注册的事件,监听者不是父组件,
             * 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
             */
            initEvents(vm); 
            // 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数
            initRender(vm); 
            callHook(vm, 'beforeCreate'); 
            // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象
            // 然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
            initInjections(vm); 
            initState(vm);  // 数据响应式的重点,处理 props、methods、data、computed、watch
            //provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性,用于组件之间通信。
            initProvide(vm); 
            callHook(vm, 'created'); 

            //浏览器 性能监听
            if ("development" !== 'production' && config.performance && mark) {
                vm._name = formatComponentName(vm, false);
                mark(endTag);
                measure(("vue " + (vm._name) + " init"), startTag, endTag);
            }

           // 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需 
           // 要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
            if (vm.$options.el) {
                vm.$mount(vm.$options.el);
            }
        };
}

说明1:config

config也是new Vue之前定义的,一系列配置性的对象:

image.png


4-1. resolveConstructorOptions

作用:

从组件构造函数中解析配置对象 options,并合并基类选项

执行流程:

  • 如果构造函数上有super才执行
  • 递归执行resolveConstructorOptions,传入构造函数的super,返回新的options
  • 当构造函数的superOptions不等于递归获取的options,重置构造函数的superOptions为递归获取的options
  • 如果options修改了,将构造函数的extendOptions和修改项合并
  • 将归获取的options和已经合并过的extendOptions,再次合并
  • 返回修改后的options

源码:

 function resolveConstructorOptions(
        Ctor // vm.constructor 就是构造函数本身
        ) {
        debugger
        /**
         * new Vue之前,执行initGlobalAPI的时候,Vue.options变成了
         * {
         *  components: {
                KeepAlive: {…}, Transition: {…}, TransitionGroup: {…}},
                directives: {model: {…}, show: {…}},
                filters: {},
                _base: ƒ Vue(options)
            }
         */
        var options = Ctor.options;
        // 有super属性,说明Ctor是Vue.extend构建的子类 继承的子类
        if (Ctor.super) { // 基类
            // 回调 基类 表示继承 基类,返回options
            var superOptions = resolveConstructorOptions(Ctor.super);  // 取Vue.options,参考上面展示对象
            var cachedSuperOptions = Ctor.superOptions; // 取Vue.options,参考上面展示对象
            if (superOptions !== cachedSuperOptions) { // 判断如果 基类的options不等于子类的options 的时候
                // 说明基类构造函数选项已经发生改变,需要重新设置
                Ctor.superOptions = superOptions; //让他的基类选项赋值Ctor.superOptions
                // 检查是否有任何后期修改/附加选项(#4976)
                var modifiedOptions = resolveModifiedOptions(Ctor);
                // 如果存在被修改或增加的选项,则合并两个选项
                if (modifiedOptions) {
                    //extendOptions合并拓展参数
                    extend(Ctor.extendOptions, modifiedOptions);
                }
                // 优先取Ctor.extendOptions 将两个对象合成一个对象
                options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
                if (options.name) {
                    options.components[options.name] = Ctor;
                }
            }
        }
        return options 
        /**返回参数格式
         * {
            components: {},
            data: ƒ (),
            directives: {},
            filters: {},
            template: "<div>detail message is :{{msg}}</div>",
            _Ctor: {0: ƒ},
            _base: ƒ Vue(options),
            __proto__: Object 
          }
         */
    }

4-1-1. mergeOptions

作用:

将两个对象合成一个对象 将父对象和子对象合并在一起,并且相同的key优先取值子对象

执行流程:

  • 规范子组件的名称,不符合则警告
  • 格式化child里面的Props、Inject、Direcitives,分别处理成对应规范的格式
  • 如果child是存在extends,则递归extends和parent,拿到合并后的新的parent
  • 如果child是存在mixins(数组),则递归mixins的每一项和parent,拿到合并后的新的parent
  • 通过父选项或者子选项的key,去获取strats中对应的merge方法,合并父选项和子选项中对应属性值,再把合并的值重新赋值给options对应的key。
  • 返回合并后的options(父子选项的最终合并项)

源码:

    function mergeOptions(
        parent, // 例:{components: {…}, directives: {…}, filters: {…}, _base: ƒ}
        child, // 子值 new Vue的传参, 例:{el: "#app", beforeCreate: ƒ, …}
        vm  // Vue实例
    ) {
        
        {
            //检验子组件
            checkComponents(child);
        }
        if (typeof child === 'function') {
            child = child.options;
        }
        // 规范child里面的Props、Inject、Direcitives

        //规范属性,确保所有的props的规范都是基于对象的
        normalizeProps(child, vm);

        // 将数组转化成对象 比如 [1,2,3]转化成
        normalizeInject(child, vm);

        // * normalizeDirectives获取到指令对象值。循环对象指令的值,如果是函数则把它变成dirs[key] = {bind: def, update: def} 这种形式
        normalizeDirectives(child);

        /**
         *看child是否存在extends(存在表示当前组件扩展了另外一个组件),
         递归当前的mergeOptions方法,
         parent就是当前的parent,child就是当前child的extends的值(扩展的那个组件);
         调用后覆盖parent
         */
        var extendsFrom = child.extends;
        if (extendsFrom) {
            //如递归
            parent = mergeOptions(parent, extendsFrom, vm);
        }

        /**
         * 检测child是否存在mixins,如果存在的话,递归当前的mergeOptions方法,
         * 并把最新的结果,去覆盖上一次调用mergeOptions方法的parent;
         */
        if (child.mixins) {
            for (var i = 0, l = child.mixins.length; i < l; i++) {
                parent = mergeOptions(parent, child.mixins[i], vm);
            }
        }

        // 作用是更新合并options
        // parent和child合并字段后组成新的options
        // 通过parent中的key,去取strats对应的方法,然后把值作为options对应key的值
        var options = {};
        var key;
        for (key in parent) {
            mergeField(key);
        }
        for (key in child) {
            if (!hasOwn(parent, key)) {
                mergeField(key);
            }
        }
        /**
         * 合并字段,更新options
         * strats类 有方法 el,propsData,data,provide,watch,props,
         * methods,inject,computed,components,directives,filters 。
         * 
         * 通过父选项或者子选项的key,去获取strats中对应的merge方法,然后,
         * 合并复选项和子选项中对应属性值,再把合并的值重新赋值给options对应的key。
         */
        function mergeField(key) {
            var strat = strats[key] || defaultStrat;
            options[key] = strat(parent[key], child[key], vm, key);
        }
        // 递归时,返回的options就是下一次函数的入参parent
        return options
    }
4-1-1-1. checkComponents

作用:

验证我们的组件名称符不符合规范。

验证组件名称 只能包含字母数字字符和连字符,必须以字母开头。

且组件名不是内置标签 slot,component 或者 html 原生的标签 或者svg标签

源码

    function checkComponents(options) {
        for (var key in options.components) {
            validateComponentName(key); // 传入组件名
        }
    }
    
    function validateComponentName(name) {
        if (!/^[a-zA-Z][\w-]*$/.test(name)) {
            // 组件名只能包含字母数字字符和连字符,必须以字母开头。
            warn(
                'Invalid component name: "' + name + '". Component names ' +
                'can only contain alphanumeric characters and the hyphen, ' +
                'and must start with a letter.'
            );
        }
        // 是否是内置标签 slot,component 或者 html 原生的标签 或者svg标签
        if (isBuiltInTag(name) || config.isReservedTag(name)) {
            // 不要将内置或保留的HTML元素用作组件名
            warn(
                'Do not use built-in or reserved HTML elements as component ' +
                'id: ' + name
            );
        }
    }
    
    //检查标记是否为内置标记。
    var isBuiltInTag = makeMap('slot,component', true);
    
    //保留标签 判断是不是 html 原生的标签 或者svg标签
    var isReservedTag = function (tag) {
        return isHTMLTag(tag) || isSVG(tag)
    };
4-1-1-2. normalizeProps

作用:

这个函数是处理子组件的props的

给出限制:props只支持对象和数组

并对象的props进行统一格式处理,然后重新赋值给子组件的props属性

执行流程:

  • options即传入的子组件child的配置项,例如:{ data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}" }
  • 如果options没有props项则退出
  • props如果是数组,则把子组件接收的props,从props:['myName', 'detail-id']格式变成:props: {myName: {type: null},detailId: {type: null}}
  • props如果对象,则把子组件接收的props,也处理成统一格式
  • 最后把child里面所有的props给规范化了,覆盖子组件的props属性

源码:

    function normalizeProps(
        options, // child组件配置,例:data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}"
        vm // Vue实例
        ) {
            debugger
        // 如果子组件child里面没有props(只有子组件需要和父组件通信才需要写),退出
        var props = options.props;
        if (!props) {
            return
        }
        var res = {};
        var i, val, name;
        /**
         * 数组形式的处理
         * 例: 把子组件接收的props,从props:['myName', 'detail-id']变成:
         * props: {
                myName: {
                    type: null
                },
                detailId: {
                    type: null
                }
            }
         */
        if (Array.isArray(props)) {
            i = props.length;
            while (i--) {
                val = props[i];
                if (typeof val === 'string') {
                    /*
                    把含有'-'的字符串 变成驼峰写法
                    把名称格式为“detail-id”的变为“detailId
                    */
                    name = camelize(val);

                    res[name] = { type: null };
                } else {
                    // props虽然是数组,但是数组项不是字符串的话,会警告你“使用数组语法时,props必须是字符串
                    warn('props must be strings when using array syntax.');
                }
            }
        } else if (isPlainObject(props)) {
        /**
         * 对象形式的处理
         * 例: props: {
         *          myName: String,
                    portlist: {
                        type: Array,
                        required: true,
                        default: ()=>[]
                    },
                }
            格式化后,变成:
            props: {
                    myName: { type: String },
                    portlist: {
                        type: Array,
                        required: true,
                        default: ()=>[]
                    },
                }
         */
            for (var key in props) {
                val = props[key];
                name = camelize(key);
                res[name] = isPlainObject(val)
                    ? val
                    : { type: val };
            }
        } else {
            //如果不是对象和数组则警告,所以这里也可以看出,props只支持对象和数组
            warn(
                "Invalid value for option \"props\": expected an Array or an Object, " +
                "but got " + (toRawType(props)) + ".",
                vm
            );
        }
        // 这里,就把child里面所有的props给规范化了,最后覆盖了源child的props属性
        options.props = res;
    }
4-1-1-3. normalizeInject

作用:

确保所有inject选项语法都规范化为对象格式,检查 inject 数据类型,我们知道vue接收inject的写法可以是数组的形式,也可以是对象的形式,这个函数就对格式进行处理,确保返回统一的格式

执行流程:

  • options即传入的子组件child的配置项,例如:{ data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}" }
  • 如果options没有inject项则退出
  • 给出限制:inject只支持对象和数组
  • 并对inject进行统一格式处理,然后重新赋值给子组件的inject属性

源码:

    function normalizeInject(
        options, // child组件配置,例:data: ƒ (),props: (2) ["msginfo"],template: "<div>{{ msginfo }}"
        vm // Vue实例
    ) {
        //  provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。
        /**
         * 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,
         * 不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
         */
        var inject = options.inject;
        if (!inject) {
            return
        }
        // 如果存在inject,需要置空
        // 保存格式化后的inject
        var normalized = options.inject = {};
        /**
         *  数组格式的处理
         */
        if (Array.isArray(inject)) {
            for (var i = 0; i < inject.length; i++) {
                /* 将数组转化成对象 例如inject为:['foo','bar'],转换过程为:
                 * normalized['foo']={from: 'foo'}
                 * normalized['bar']={from: 'bar'}
                   结果:{
                    foo:{from: 'foo'},
                    bar:{from: 'bar'}
                  }
                */
                normalized[inject[i]] = { from: inject[i] };
            }
        }
        /**
         *  对象格式的处理
         */
        else if (isPlainObject(inject)) {
            for (var key in inject) {
                /**如果inject为:
                {
                    foo: {
                        from: 'name1',
                        default: 'name1'
                    },
                    bar
                }
                 * 转换过程为:
                  第一次遍历,key为 foo,val为{from: 'name1',default: 'name1'},是个对象,
                  则执行extend合并{ from: 'foo' }和{from: 'name1',default: 'name1'}
                  结果:
                  {
                     foo: {
                        from: 'name1',
                        default: 'name1'
                    },
                    bar:{from: bar}
                  }
                 */
                var val = inject[key];
                normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val };
            }
        } else {
            warn(
                "Invalid value for option \"inject\": expected an Array or an Object, " +
                "but got " + (toRawType(inject)) + ".",
                vm
            );
        }
    }
4-1-1-4. normalizeDirectives

作用:

将原始函数指令归一化为对象格式。循环对象指令的值,如果是函数转换。

例如:

options.directives为:

{ getList: a(), delete: b()}

转换为:

{ 
   getList: { bind: a(), update: a() },
   delete: { bind: b(), update: b() }
}

执行流程:

  • 如果指令存在
  • 遍历指令数组,如果遍历项是函数,才对它进行转换

源码:

  function normalizeDirectives(
        options // 入参就是child
    ) {
        debugger
        //获取参数中的指令
        var dirs = options.directives;
        if (dirs) { //如果指令存在
            for (var key in dirs) {
                var def = dirs[key];  //获取到指令的值
                if (typeof def === 'function') { //如果是函数
                    //为该函数添加一个对象和值
                    /**例如:options.directives为{ getList: a(), delete: b()},转换为
                     { 
                         getList: { bind: a(), update: a() },
                         delete: { bind: b(), update: b() }
                     }
                     */
                    dirs[key] = { bind: def, update: def };
                }
            }
        }
    }
4-1-1-5. mergeField

作用:

合并更新options。

通过父选项或者子选项的key,去获取strats中对应的merge方法。

然后,合并父选项和子选项中对应属性值,再把合并的值重新赋值给options对应的可以。

源码:

        function mergeField(key) {
            var strat = strats[key] || defaultStrat;
            options[key] = strat(parent[key], child[key], vm, key);
        }

strats定义在new Vue之前,它可以说了包含了vue所需要的所有东西,如生命周期,data,watch,el等一系列merge函数。

image.png


4-2. initProxy

proxy比较重要,后续我们将单独开一篇文章讲解proxy

作用:

如果Proxy属性存在,则把包装后的vm属性赋值给_renderProxy属性值,否则把vm是实例本身赋值给_renderProxy属性。

执行流程:

  • 判断 系统内置 函数有没有 es6的Proxy 代理对象api,无则将 vm直接赋值到 vm._renderProxy
  • 有则继续,根据vm.$options是否存在render来选择代理处理程序

源码:

//初始化 代理 监听
initProxy = function initProxy(vm) {
            // 判断 系统内置 函数有没有 es6的Proxy 代理对象api
            // var hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy);
            if (hasProxy) {
                // 确定使用哪个代理处理程序
                var options = vm.$options;
                var handlers = options.render && options.render._withStripped
                    ? getHandler  // 获取值
                    : hasHandler;  // 判断内部函数,这样vue中模板就可以使用内置函数
                //实例化 代理对象,只是这里添加了 警告的日志而已
                vm._renderProxy = new Proxy(vm, handlers);
            } else {
                //如果不能代理直接赋值
                vm._renderProxy = vm;
            }
};


/**
* hasHandler方法的应用场景在于查看vm实例是否拥有某个属性—比如调用for in循环遍历vm实例属性时会
* 触发hasHandler方法
*/

var hasHandler = {
            // in 操作符的捕捉器,has 方法返回一个 boolean 属性的值.
            has: function has(target, key) {
                debugger
                var has = key in target;
                //是否含有全局api 就是window 的内置函数
                //全局api
                // var allowedGlobals = makeMap(
                //     'Infinity,undefined,NaN,isFinite,isNaN,' +
                //     'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
                //     'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
                //     'require' // for Webpack/Browserify
                // );

                var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
                //如果 key 在target对象中 不存在, 且不是(全局api 或者 第一个字符不是_的时候) 发出警告
                // 确定属性名称是否可用
                if (!has && !isAllowed) {
                    warnNonPresent(target, key);
                }
                // 返回true
                return has || !isAllowed
            }
};

// 该方法可以在开发者错误的调用vm属性时,提供提示作用。
var getHandler = {
            // 属性读取操作的捕捉器。
            get: function get(target, key) {
                // key如果是字符串 并且 key不在target中发出警告
                if (typeof key === 'string' && !(key in target)) {
                    warnNonPresent(target, key);
                }
                //返回target值
                return target[key]
            }
};
        

4-3. initLifecycle

作用:

初始化vm实例中和生命周期相关的属性,例如:

  • 挂载$parent,可以通过this.$parent来直接访问到父组件
  • 挂载$refs,可以通过this.$refs来直接访问已注册过 ref 的所有子组件
  • 以及其它属性

源码:

initLifecycle(vm)

function initLifecycle(vm) {
        debugger
        var options = vm.$options;
        // 子组件的父实例
        var parent = options.parent;
        // 当父实例存在,且该实例不是抽象组件
        if (parent && !options.abstract) {
            // 判断parent父亲节点是否存在,并且判断抽象节点是否存在
            while (parent.$options.abstract && parent.$parent) {
                // 找到顶层的parent
                parent = parent.$parent;
            }
            // 子节点添加 vm,父实例里卖弄存放当前实例
            parent.$children.push(vm);
        }
        //指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent访问父实例
        vm.$parent = parent; 
        //当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
        vm.$root = parent ? parent.$root : vm; 
        vm.$children = []; //当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。
        vm.$refs = {}; // 一个对象,持有已注册过 ref 的所有子组件。
        vm._watcher = null; // 组件实例相应的 watcher 实例对象。
        vm._inactive = null; // 表示keep-alive中组件状态,如被激活,该值为false,反之为true。
        vm._directInactive = false;  // 不活跃 禁用的组件标志 也是表示keep-alive中组件状态的属性。
        vm._isMounted = false; // 当前实例是否完成挂载(对应生命周期图示中的mounted)。
        vm._isDestroyed = false; //当前实例是否已经被销毁(对应生命周期图示中的destroyed)。
        //当前实例是否正在被销毁,还没有销毁完成(介于生命周期图示中deforeDestroy和destroyed之间)。
        vm._isBeingDestroyed = false; 
}

4-4. initEvents

作用:

当vm.$options._parentListeners(父组件绑定在当前组件上的事件)存在时才会执行组件事件更新操作。

更新数据源 并且为新的值 添加函数 旧的值删除函数等功能

源码:

initEvents(vm)

function initEvents(vm) {
        debugger
        // 父组件绑定在当前组件上的事件
        vm._events = Object.create(null);
        vm._hasHookEvent = false;
        // 父组件绑定在当前组件上的事件
        var listeners = vm.$options._parentListeners;
        if (listeners) {
            //更新组件事件
            updateComponentListeners(vm, listeners);
        }
}

function updateComponentListeners(
        vm,  // vue实例对象
        listeners,  // 父组件绑定在当前组件上的事件对象
        oldListeners // 当前组件上旧的事件对象
    ) {
        debugger
        target = vm;
        //更新数据源 并且为新的值 添加函数 旧的值删除函数等功能
        updateListeners(listeners, oldListeners || {}, add, remove$1, vm);
        target = undefined;
}

这里最终走到updateListeners函数,有关这个函数的讲解,在【文章1里面】

4-5. initRender

作用:

这个函数的主要功能是:

1、获取_parentVnode对象(子组件存在)

2、将vnode解析为slot对象

3、挂载 '_c'函数(生成vnode)

4、调用defineReactive(将在响应式章节讲解)函数拦截实例的 'attrsattrs' 和 'listeners' 属性的操作

源码:

    function initRender(vm) {
        vm._vnode = null; // 上一个 vonde
        vm._staticTrees = null; // v-once缓存的树
        var options = vm.$options; // 获取参数
        var parentVnode = vm.$vnode = options._parentVnode; // 父树中的占位符节点
        var renderContext = parentVnode && parentVnode.context; // this 上下文
        // 执行resolveSlots获取占位符VNode下的slots信息 
        // 
        debugger
        /**
         * 执行resolveSlots获取占位符VNode下的slots信息,如这里的div节点。
         * renderContext是Vue实例,options._renderChildren是一个slot对应vnode数组。
         * 例如:
         * [
            {
                { tag: "div", data: {attrs: {…}, slot: "you"}...},
                { tag: undefined, data: undefined...},
                { tag: "div", data: {attrs: {…}, slot: "my"}...},
                { tag: undefined, data: undefined...}
            }
          ]
           执行后vm.$slots格式为: { you: [VNode], my: [VNode], default: [VNode, VNode] }
         */
        vm.$slots = resolveSlots(options._renderChildren, renderContext);
        vm.$scopedSlots = emptyObject;
        //将createElement fn绑定到这个实例
        //这样我们就得到了合适的渲染上下文。
        //内部版本由模板编译的呈现函数使用
        //创建虚拟dom的数据结构
        vm._c = function (a, b, c, d) {
            return createElement(
                vm, //vm  new Vue 实例化的对象
                a, //有可能是vonde或者指令
                b,
                c,
                d,
                false
            );
        };
        //用户编写的渲染功能。
        vm.$createElement = function (a, b, c, d) {
            return createElement(vm, a, b, c, d, true);
        };
        // $attrs和$listener将被公开,以便更容易地进行临时创建。
        var parentData = parentVnode && parentVnode.data;
        {
            // 在object上定义一个响应式的属性,这个方法,就是把对象obj里的属性key变成一个getter/setter形式的
            // 响应式的属性 同时在getter的时候收集依赖,并在setter的时候触发依赖。

            // 我们将在响应式的章节分析它
            defineReactive(
                vm,
                '$attrs',
                parentData && parentData.attrs || emptyObject,
                function () {
                    !isUpdatingChildComponent && warn("$attrs is readonly.", vm);
                },
                true
            );
            // 通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改
            defineReactive(vm, '$listeners', options._parentListeners || emptyObject, function () {
                !isUpdatingChildComponent && warn("$listeners is readonly.", vm);
            }, true);
        }
    }

4-5-1. resolveSlots

以下分析基于下面的例子。 父组件

// 插入了2个具名插槽,和一个非具名插槽
<msg-tip :msginfo='msgText' :totalnum='total'>
      <div slot='you'>你</div>
      <div slot='my'>我</div>
      他
</msg-tip>

子组件

<div>
    {{ msginfo }}存了¥{{ totalnum }}
    <slot name='you'></slot>
    <slot name='my'></slot>
    <slot></slot>
</div>

作用:

【此函数为slot插槽的核心】

收集父组件里的所有插槽,并对具名插槽和非具名插槽分类,同时将插槽对应的Vnode放置在对应的插槽下面,返回一个封装对象。

执行流程:

  • 传入slot占位符对应的Vnode的集合(例:[{tag: "div", data: {…}...})如:
    [
            {
                { tag: "div", data: {attrs: {}, slot: "you"}...},
                { tag: undefined, data: undefined...},
                { tag: "div", data: {attrs: {}, slot: "my"}...},
                { tag: undefined, data: undefined...}
            }
    ]
    
  • 声明一个slots来放入所有的插槽
  • 遍历children,拿到slot节点的data值,如果节点含有slot属性,即具名插槽,需要删除attrs中的slot属性
  • 判断是否为具名插槽,如果为具名插槽,将具名插槽的name存放到对应的属性名下,如果为非具名插槽,统一放在default数组下
  • 最终返回slots(例:{ you: [VNode], my: [VNode], default: [VNode, VNode] }

源码:

    function resolveSlots(
        children, //存放  slot占位符对应的Vnode 的集合
        context // 上下文,vue实例
        ) {
        var slots = {}; // 缓存插槽
        // 如果没有子节点 则返回一个空对象
        // 一个组件可以有多个slot,所以children是个数组
        if (!children) {
            return slots
        }
        //循环slot的vnode集合
        // slots是对象,slot是数组(存储vnode),slots存储slot
        for (var i = 0, l = children.length; i < l; i++) {
            var child = children[i];
            // slot占位符对应数据
            // data 例: { attrs: {slot: "you"}, slot: "you" }
            var data = child.data; 
            // 如果节点含有slot属性,即具名插槽,需要删除attrs中的slot属性
            if (data && data.attrs && data.attrs.slot) {
                delete data.attrs.slot;
            }
            /* 判断是否为具名插槽,如果为具名插槽,将具名插槽的name存放到对应的属性名下,
                如果为非具名插槽,统一放在default数组下
            */
            if ((child.context === context || child.fnContext === context) &&
                data && data.slot != null
            ) {
                var name = data.slot; // 插槽名
                // 如果slots[name]不存在,则slots设置插槽名属性,初始化为一个空数组
                // 并设置slot为空数组,这里用引用类型将slots和slot关联起来了,当我们设置slot时,
                // 就会自动同步到slots对应的插槽下面
                var slot = (slots[name] || (slots[name] = []));
                if (child.tag === 'template') {  // 此例tag为div
                    //把子节点的 子节点 添加 到slot插槽中
                    slot.push.apply(slot, child.children || []);
                } else {
                    // 把slot占位符对应的Vnode 添加 到slot插槽中
                    slot.push(child);
                }
            } else {
                // 如果是非具名slot,则把slot对应的节点插入到slots默认的default数组中
                (slots.default || (slots.default = [])).push(child);
            }
        }
        //忽略只包含空白的槽
        for (var name$1 in slots) {
            //删除空的插槽
            if (slots[name$1].every(isWhitespace)) {
                delete slots[name$1];
            }
        }
        debugger
        return slots
    }

4-6. callHook(vm)

callHook属于声明周期相关的函数了,我们将在声明周期篇详情讲解。

4-7. initInjections(vm)

用的不多,暂时不讲

4-8. initProvide(vm)

用的不多,暂时不讲

4-9. initState(vm)

这个函数会在【vue源码分析【3】- vue响应式】中讲解

4-10. $mount(vm)

作用

首先缓存了原型上的 $mount 方法,再重新定义该方法,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。

如果没有render函数,则获取template,template可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。 compileToFunctions是对我们最后生成的模板进行解析,生成render函数。

其实vue源码在2个地方定义了$mount 方法,分别是文件中间的位置和文件末尾的文字,这里先执行的末尾的位置的 $mount 方法。为什么要这样做呢?

末尾的是适用于 Runtime+Compiler 版本的。中间的mount 的 Vue.prototype.$mount 方法是适用于 Runtime Only 版本的。

我们这里先解析末尾的$mount,因为按照我们代码的流程是会先走这里

源码:

    vm.$mount(vm.$options.el);
    
    const mount = Vue.prototype.$mount
    
    Vue.prototype.$mount = function (
        el,  // 例:#app
        hydrating // 服务端渲染相关,在浏览器环境下我们不需要传
        ) { 
        // 获取dom,已经是dom就返回,不是dom并且获取不到,警告提示,创建一个新的dev
        el = el && query(el); 
        /* istanbul ignore if */
        //如果el 是body 或者文档 则警告
        if (el === document.body || el === document.documentElement) {
            "development" !== 'production' && warn(
                /**
                * 不要将<html>或<body>挂载到vue的mount,而是需要挂载普通元素
                * 因为挂载是覆盖的,如果挂载在body或html上, 覆盖后就没有body和html节点了
                * 所以我们一般采用的都是挂载在div上的形式。
                */
                "Do not mount Vue to <html> or <body> - mount to normal elements instead."
            );
            return this
        }
        //获取参数
        var options = this.$options;
        // resolve template/el and convert to render function
        //解析模板/el并转换为render函数
        if (!options.render) {
            //获取模板字符串
            var template = options.template;
            if (template) { //如果有模板
                if (typeof template === 'string') {
                    //模板第一个字符串为# 则判断该字符串为 dom的id
                    if (template.charAt(0) === '#') {
                        template = idToTemplate(template); //获取字符串模板的innerHtml
                        /* istanbul ignore if */
                        if ("development" !== 'production' && !template) {
                            warn(
                                ("Template element not found or is empty: " + (options.template)),
                                this
                            );
                        }
                    }
                } else if (template.nodeType) { //如果template 是don节点 则获取他的html
                    template = template.innerHTML;
                } else {
                    //如果什么都是不是则发出警告
                    {
                        warn('invalid template option:' + template, this);
                    }
                    return this

                }
            } else if (el) {
                //如果模板没有,dom节点存在则获取dom节点中的html 给模板
                /**  template 例子如下:
                 * "<div id="app">
                        <!--this is comment--> {{ message }}
                    </div>"
                 */
                template = getOuterHTML(el);

            }
            if (template) {
                /* istanbul ignore if */
                //监听性能监测
                if ("development" !== 'production' && config.performance && mark) {
                    mark('compile');
                }
                //创建模板
                var ref = compileToFunctions(
                    template, //模板字符串
                    {
                         //IE在属性值中编码换行,而其他浏览器则不会
                        shouldDecodeNewlines: shouldDecodeNewlines, //flase 
                        //true chrome在a[href]中编码内容
                        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, 
                         //改变纯文本插入分隔符。修改指令的书写风格,比如默认是{{mgs}} 
                         //delimiters: ['${', '}']之后变成这样 ${mgs}
                        delimiters: options.delimiters,
                        //当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
                        comments: options.comments 
                    },
                    this
                );
                //ast 模板
                //code 虚拟dom需要渲染的参数函数
                //staticRenderFns 【说明1 staticRenderFns】
                //这样赋值可以有效地 防止 引用按地址引用,造成数据修改而其他对象也修改问题,
                var render = ref.render;
                var staticRenderFns = ref.staticRenderFns;
                /*
                 render 是  虚拟dom,需要执行的编译函数
                 */
                options.render = render;
                options.staticRenderFns = staticRenderFns;
                /* istanbul ignore if */
                if ("development" !== 'production' && config.performance && mark) {
                    mark('compile end');
                    measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
                }
            }
        }
        // 调用原先原型上的 $mount 方法挂载
        // 再次执行mount,执行的是中间位置定义的$mount
        // 这一步执行完,data就已经渲染到页面了
        return mount.call(
            this, // Vue实例
            el, //真实的dom 例:el = div#app {__vue__: null, align: "", title: "", lang: "", 
                // translate: true, …}
            hydrating //undefined
        )
    };

【说明1 staticRenderFns】

这里的staticRenderFns目前是一个空数组,其实它是用来保存template中,静态内容的render,比如模板为:

<div id="app">
    <p>这是<span>静态内容</span></p>
    <p>{{message}}</p>
</div>

staticRenderFns为:

staticRenderFns = function () {
    with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])
}

5. compileToFunctions

5-1. 基本信息

作用:

编译相关函数

为了方便查看,我调整了声明的顺序,并且简化了代码,我们先对调用结构有个了解。这里的函数调用看着有点绕,实际就是做了柯里化,实现了默认传参。

源码:

// 【第一步】
// 调用compileToFunctions函数,传进3个参数
// 下一步我们先分析compileToFunctions做了什么操作
var ref = compileToFunctions(
            template,
            {
              shouldDecodeNewlines: shouldDecodeNewlines,
              linesForHref: shouldDecodeNewlinesForHref,
              delimiters: options.delimiters, 
              comments: options.comments
            },
            this
);

// 【第二步】
// 由第一步知道,compileToFunctions是个函数,所以
// ref$1.compileToFunctions返回的也肯定是个函数
// 同时也说明ref$1本身是返回一个对象
var compileToFunctions = ref$1.compileToFunctions;
// 转换为:var ref = ref$1.compileToFunctions(template,{...},this)
    
// 【第三步】
// 说明 createCompiler函数返回的是一个对象
// 入参baseOptions是在new Vue之前定义的
var ref$1 = createCompiler(baseOptions);
// 继续转换为:var ref = createCompiler(baseOptions).compileToFunctions(template,{...},this)

// 【第四步】
// 由第三步,可以知道,createCompilerCreator函数返回的也是一个对象
var createCompiler = createCompilerCreator(
     function baseCompile(template,options) {
            return {
                ast: ast, //ast 模板
                render: code.render, //code 虚拟dom需要渲染的参数函数
                staticRenderFns: code.staticRenderFns  //空数组
            }
     });
}
/**
*继续转换为:
var ref = createCompilerCreator(function baseCompile(){template,options})
(baseOptions).compileToFunctions(template,{...},this)
现在终于调用到了createCompilerCreator函数,我们接着看createCompilerCreator返回了什么
*/

// compile函数返回了compiled,compiled又是由入参函数baseCompile返回的,
// 即前面的createCompiler 里面的参数函数function baseCompile(template,options) {});
// 返回一个函数,该函数又返回了一个封装对象:{  ast: ast, render...,staticRenderFns:... },
// 即compile函数也返回了这个对象
function createCompilerCreator(
            baseCompile 
        ) {
            return function createCompiler(baseOptions) {
               function compile(template,options) {
                    var compiled = baseCompile(
                        template,
                        finalOptions
                    );
                    return compiled
                }
                return {
                    compile: compile,
                    compileToFunctions: createCompileToFunctionFn(compile)
                }
           }
}

// 由上一步可知,compile是个函数,并且返回一个对象 
/**
* createCompilerCreator(function baseCompile(){template,options})
  (baseOptions).compileToFunctions其实就是取createCompilerCreator返回的对象的compileToFunctions
  属性,即createCompileToFunctionFn(compile)的值
  所以,可以再次转换:
* var ref = createCompileToFunctionFn(function baseCompile(){template,options})
            (baseOptions).compileToFunctions(template,{...},this)
            
* 没看错,这就是我们终极转换的函数执行链,现在就很清晰了,createCompileToFunctionFn函数的入参compile
* 就是function baseCompile(){template,options},
* compileToFunctions的入参就是上面compileToFunctions函数的参数(template,{...},this),即第一步的入参
*/
function createCompileToFunctionFn(compile) {
        return function compileToFunctions(
            template,
            options,
            vm 
        ) {
            var compiled = compile(
                template,
                options
            );
            return (cache[key] = res)
        }
}

接下来我们就可以仔细分析了

5-2. createCompilerCreator

作用:

在render 函数中编译模板字符串

执行流程:

来看下函数是createCompilerCreator怎么定义的,这里的baseCompile就是上面createCompilerCreator函数的入参:baseCompile,里面的baseOptions(new Vue之前定义的)也是从前面的函数传递过来的。最终所有的逻辑都会走到这个函数,前面的逻辑可以认为是提供函数参数的操作。

  • 传入一个编译函数,返回一个对象
  • 这个对象有2个属性,分别是:
    • 定义的compile函数:编译器,在 render 函数中编译模板字符串,负责将模版字符串(即你编写的类 html 语法的模版代码)编译为 JavaScript 语法的 render 函数。
    • createCompileToFunctionFn: 入参为定义的compile函数,解析模板字符串成为函数

所以这个函数的核心其实就是里面定义的compile函数。

源码:

    // 最终返回compile函数
        function createCompilerCreator(
            baseCompile // 基本的编译函数,传入这个参数是为了让我们在当前函数
                        // 能调用传入函数,同时给他传参
        ) {
            return function createCompiler(baseOptions) { //[图1]
                debugger
                function compile(
                    template, // 例:"<div id="app">{{ message }} </div>" [图2]
                    options   // 例:{shouldDecodeNewlines: false...} [图3]
                ) {
                    debugger
                    // 创建一个对象 拷贝baseOptions 拷贝到 原型 protype 中
                    // [图4]
                    var finalOptions = Object.create(baseOptions); //为虚拟dom添加基本需要的属性
                    var errors = [];
                    var tips = [];
                    // 警告函数
                    finalOptions.warn = function (msg, tip) {
                        (tip ? tips : errors).push(msg);
                    };

                    if (options) {
                        // 合并modules到finalOptions
                        if (options.modules) { 
                            finalOptions.modules = (baseOptions.modules || []).concat(options.modules);
                        }
                        // 合并指令到finalOptions
                        if (options.directives) {
                            finalOptions.directives = extend(Object.create(baseOptions.directives || null), options.directives);
                        }

                        // 复制其他选项到finalOptions
                        for (var key in options) {
                            if (key !== 'modules' && key !== 'directives') {
                                //浅拷贝
                                finalOptions[key] = options[key];
                            }
                        }
                    }
                    /**
                     * 这2个参数会作为参树传入到:createCompiler = 
                     * createCompilerCreator(function baseCompile(template, options) {})中的
                     * template, options中
                     */
                    //【图4  compiled】
                    var compiled = baseCompile(
                        template, 
                        finalOptions // 为虚拟dom添加基本需要的属性
                    );


                    {
                        errors.push.apply(errors, detectErrors(compiled.ast));
                    }
                    compiled.errors = errors;
                    compiled.tips = tips;
                    return compiled
                }

                /*
                * compile
                *在 render 函数中编译模板字符串。只在独立构建时有效
                var res = Vue.compile('<div><span>{{ msg }}</span></div>')
                new Vue({
                data: {
                    msg: 'hello'
                },
                    render: res.render,
                    staticRenderFns: res.staticRenderFns
                })
                *
                *
                *
                * */
               debugger
                return {
                    compile: compile,
                    compileToFunctions: createCompileToFunctionFn(compile)
                }
            }
        }

[图1] baseOptions:

image.png

[图2] template:

image.png

[图3] options:

image.png

【图4 compiled】

image.png

[图4] finalOptions:

image.png

5-3. baseCompile

作用:

baseCompile函数是回调时才触发的。

源码:

    //编译器创建的创造者
        var createCompiler = createCompilerCreator(

        //把html变成ast模板对象,然后再转换成 虚拟dom 渲染的函数参数形式。
        // 返回出去一个对象
        // {ast: ast, //ast 模板
        // render: code.render, //code 虚拟dom需要渲染的参数函数
        //staticRenderFns: code.staticRenderFns } //空数组

        function baseCompile(
            template, // "<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
            options // 这里已经baseOptions和options的合并项了 【图1 】
        ) {
            
            //返回ast模板对象 
            // parse函数非常关键,我们将在下一节解析
            // 【图2 ast】
            var ast = parse(template.trim(), options);
            // optimize 的主要作用是标记 static 静态节点
            // optimize 也将在后面的章节解析
            if (options.optimize !== false) {  
                // * 循环递归虚拟node,标记是不是静态节点
                //*  根据node.static或者 node.once 标记staticRoot的状态
                // 我们将在下一节解析
                optimize(ast, options);
            }
            //初始化扩展指令,on,bind,cloak,方法, dataGenFns 获取到一个数组,
            // 数组中有两个函数genData和genData$1
            //genElement根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,
            // 或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
            //返回对象{ render: ("with(this){return " + code + "}"),staticRenderFns: state.staticRenderFns} //空数组
            // 我们将在下一节解析
            // 【图3 code】
            var code = generate(ast, options);

            return {
                ast: ast, //ast 模板
                render: code.render, //code 虚拟dom需要渲染的参数函数
                staticRenderFns: code.staticRenderFns  // 静态渲染函数,数组
            }
     });

【图1 options】

image.png

【图2 ast】

image.png

【图3 code】

image.png


5-4. createCompileToFunctionFn

作用:

createCompilerCreator最后面就是执行createCompileToFunctionFn

源码:

       function createCompileToFunctionFn(compile) {
        
        var cache = Object.create(null);
        /**
         * 这里又返回了一个函数,父函数作为属性值在createCompilerCreator中:
         * {
                compile: compile,
                compileToFunctions: createCompileToFunctionFn(compile)
            }

         * 所以createCompileToFunctionFn(compile)可以看作是执行了当前的返回函数:
            compileToFunctions(template, options, vm )
        //  * 我们往前推,可以看到上面那个对象的compileToFunctions属性是在mount的ref中
        //     调用的,因此,compileToFunctions的入参就是定义ref时传入的
        //  * 
        */
            
        return function compileToFunctions(
            // 这几个入参就是compileToFunctions: createCompileToFunctionFn(compile)中
            // 的compile返回的三个参数
            template,  //  "<div id=\"app\">
            options,  // {shouldDecodeNewlines: false,...}
            vm  // Vue实例
        ) {
            
            //浅拷贝参数
            options = extend({}, options);
            //警告
            var warn$$1 = options.warn || warn;
            //删除参数中的警告
            delete options.warn;

            /*
             *这个选项只在完整构建版本中的浏览器内编译时可用。
             * 详细:改变纯文本插入分隔符。
             *
             * 示例:
             new Vue({
                delimiters: ['${', '}']
             })
             // ['${', '}'] String后变成了 "${,}"
             */

            var key = options.delimiters ? String(options.delimiters) + template : template;
            if (cache[key]) {
                return cache[key]
            }
            
            // compile 传进来的函数, 返回值就是baseCompile函数返回值
            var compiled = compile(
                template, //模板字符串
                options //参数
            );

            // turn code into functions 将代码转换为函数
            var res = {};
            var fnGenErrors = [];
            //将compiled.render创建一个函数,如果发生错误则记录fnGenErrors错误
            //把字符串 转化成真正的js并且以 函数的方式导出去
            // 【说明1 createFunction】
            // 【图1 res】
            res.render = createFunction(
                compiled.render,
                fnGenErrors);
            
            res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
                return createFunction(code, fnGenErrors)
            });
            
            // 【图2 3】
            return (cache[key] = res)
        }
    } 

5-6. createFunction

作用:

把字符串 转成真正的js 并且以一个函数形式导出去

源码:

    function createFunction(
        code, // 例:"with(this){return _c('div',{attrs:{"id":"app"}},[_v(" "+_s(message)+"\n    ")])}"
        errors
        ) {
        debugger
        try {
            return new Function(code)
            /**
             * 转换成函数:
             * (function anonymous() {
                with(this){return _c('div',{attrs:{"id":"app"}},[_v(" "+_s(message)+"\n    ")])}
                })
             */
        } catch (err) {
            errors.push({ err: err, code: code });
            return noop
        }
    }

【图1 res.render,可以看到res.render已经是个函数了

image.png

【图2】cache,以template为key的对象

image.png

【图3】res,渲染函数对象

image.png

到这里,整个编译的过程就解析完成了。

5-8. mount

当我们在末尾的mount执行:

return mount.call(
       this, // Vue实例
       el, //真实的dom 例:el = div#app {__vue__: null, align: "", title: "", lang: "", translate: true, …}
       hydrating //undefined
)

它会继续执行mount,不过此时的mount是中间定义的mount,如下:

    Vue.prototype.$mount = function (
        el, 
        hydrating
        ) {
        // query(el) 获取dom,已经是dom就返回,不是dom并且获取不到,警告提示,创建一个新的dev
        el = el && inBrowser ? query(el) : undefined;
        return mountComponent(
            this, // Vue实例
            el,  // 真实dom 例:el = div#app {align: "", title: "", lang: "", translate: true, dir: "", …}
            hydrating
        )
    };

mountComponent:

如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程

mountComponent 核心就是先调用 vm._render 方法,先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。

    //安装组件
    function mountComponent(
        vm,  //Vue 实例
        el,  //真实dom
        hydrating //新的虚拟dom vonde
    ) {
        debugger
        // 将真实dom挂载到vue实例上去
        vm.$el = el;
        /*
        Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,
        前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,
        需要借助 webpack 的 vue-loader 事先把模板编译成 render函数。
         */

        //如果参数中没有渲染函数,说明使用的是 Runtime only
        if (!vm.$options.render) { //实例化vm的渲染函数,虚拟dom调用参数的渲染函数
            //创建一个空的组件
            vm.$options.render = createEmptyVNode;

            {
                /* istanbul ignore if */
                //如果参数中的模板第一个不为# 号则会 警告,因为Runtime only版本的代码是这种格式:el: '#app',
                if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
                    vm.$options.el || el) {
                    /*
                        您使用的是 Runtime only 生成的Vue,其中模板编译器不可用。
                        或者将模板预编译为渲染函数,或使用内置的编译器。
                    */
                    warn(
                        'You are using the runtime-only build of Vue where the template ' +
                        'compiler is not available. Either pre-compile the templates into ' +
                        'render functions, or use the compiler-included build.',
                        vm
                    );
                } else {
                    // 无法装载组件:未定义template或render函数
                    warn(
                        'Failed to mount component: template or render function not defined.',
                        vm
                    );
                }
            }
        }

        //执行生命周期函数 beforeMount
        callHook(vm, 'beforeMount');
        //更新组件
        var updateComponent;
        /* istanbul ignore if */
        //如果开发环境
        /*
            Vue.config.performance为true的话,以在浏览器开发工具的性能/时间线面板中
            启用对组件初始化、编译、渲染和打补丁的性能追踪
        */
        
        if ("development" !== 'production' && config.performance && mark) {
            updateComponent = function () {
                var name = vm._name;
                var id = vm._uid;
                var startTag = "vue-perf-start:" + id;
                var endTag = "vue-perf-end:" + id;

                mark(startTag); //插入一个名称 并且记录插入名称的时间
                var vnode = vm._render();
                mark(endTag);
                measure(("vue " + name + " render"), startTag, endTag);

                mark(startTag); //浏览器 性能时间戳监听
                //更新组件
                vm._update(vnode, hydrating);
                mark(endTag);
                measure(("vue " + name + " patch"), startTag, endTag);
            };
        } else {
            updateComponent = function () {
                //直接更新view试图
                // 【在第一篇 new Vue有讲解】
                vm._update(
                    /*
                     render 是  虚拟dom,需要执行的编译函数 类似于这样的函数
                     (function anonymous( ) {
                     with(this){return _c('div',{attrs:{"id":"app"}},[_c('input',{directives:
                        [{name:"info",rawName:"v-info"},{name:"data",rawName:"v-data"}],
                        attrs:{"type":"text"}}),_v(" "),_m(0)])}
                     })
                     */
                    vm._render(), //先执行_render,返回虚拟 Node
                    hydrating
                );
            };
        }

        // we set this to vm._watcher inside the watcher's constructor
        // since the watcher's initial patch may call $forceUpdate (e.g. inside child
        // component's mounted hook), which relies on vm._watcher being already defined
        //我们将其设置为vm。在观察者的构造函数中
        //因为观察者的初始补丁可能调用$forceUpdate(例如inside child)
        //组件的挂载钩子),它依赖于vm。_watcher已经定义
        //创建观察者
        new Watcher(
            vm,  //vm vode
            updateComponent, //数据绑定完之后回调该函数。更新组件函数 更新 view试图
            noop, //回调函数
            null, //参数
            true //是否渲染过得观察者
            /* isRenderWatcher */
        );
        hydrating = false;

        // manually mounted instance, call mounted on self
        // mounted is called for render-created child components in its inserted hook
        //手动挂载实例,调用挂载在self上
        // 在插入的钩子中为呈现器创建的子组件调用// mount
        if (vm.$vnode == null) {
            vm._isMounted = true;
            //执行生命周期函数mounted
            // 渲染data
            callHook(vm, 'mounted');
        }
        debugger
        return vm
    }

5-9 总结

最终我们的ref是这样的:

image.png

然后挂载ref上的属性到options上:

image.png

6. optimize

作用: 对parse解析后的AST进行了优化,标记了静态节点和静态根节点。当一个节点staticRoots(静态根节点)为true,并且不在v-for中,那么第一次render的时候会对以这个节点为根的子树进行缓存,等到下次再render的时候直接从缓存中拿,避免再次render。

优化器的目标:遍历生成的模板AST树,检测纯静态的子树,即永远不需要更改的DOM。一旦我们检测到这些子树,我们可以:

  1. 把它们变成常数,这样我们就不需要在每次重新渲染时为它们创建新的节点;
  2. 在修补过程中完全跳过它们。循环递归虚拟node,标记是不是静态节点。根据node.static或者 node.once 标记staticRoot的状态

源码:

var createCompiler = createCompilerCreator(
function baseCompile(
            template, // "<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
            options // 这里已经baseOptions和options的合并项了
        ) {
            var ast = parse(template.trim(), options);
            if (options.optimize !== false) {  
                // * 循环递归虚拟node,标记是不是静态节点
                //*  根据node.static或者 node.once 标记staticRoot的状态
                optimize(ast, options);
}

function optimize(
    root,  // 转换后的ast树
    options // 配置项
) {
        if (!root) {
            return
        }
        //匹配type,tag,attrsList,attrsMap,plain,parent,children,attrs + staticKeys 字符串
        // 例:options.staticKeys = "staticClass,staticStyle"
        isStaticKey = genStaticKeysCached(options.staticKeys || '');
        //保留标签 判断是不是真的是 html 原有的标签 或者svg标签
        isPlatformReservedTag = options.isReservedTag || no;
        
        // 第一步: 标记所有非静态节点。
        markStatic$1(root);
        
        //第二步: 标记所有的静态根节点
        markStaticRoots(root, false);
}

6-1 markStatic$

作用:

第一步,标记所有非静态节点。

分别循环递归子节点和ifConditions虚拟node,标记是否静态节点,如果子节点是非静态,则父节点也是非静态的。

源码:

    function markStatic$1(node) {
        debugger
        // 初步判断这个节点的是否可以为静态节点。
        node.static = isStatic(node);   
        if (node.type === 1) {
            // 不要将组件插槽内容设置为静态。这就避免了:
            // 1.组件无法更改插槽节点
            // 2.静态插槽内容无法热加载
            if (
                !isPlatformReservedTag(node.tag) &&   //判断是不是html 原有标签(div等) 或者svg标签
                node.tag !== 'slot' &&  //当前标签不等于slot
                node.attrsMap['inline-template'] == null  // 也不是inline-template    内联模板
            ) {
                return
            }
            // 递归循环子节点,如果子节点不是静态节点,则父节点也不是
            for (var i = 0, l = node.children.length; i < l; i++) {
                var child = node.children[i];
                markStatic$1(child);
                if (!child.static) {
                    node.static = false;
                }
            }

            /**
             * if标记,是一个数组,如input标签的type:
             * node.ifConditions = [
                    {exp: "(_f(\"recordType\")(texts))==='checkbox'", block: {…}}
                    {exp: "(_f(\"recordType\")(texts))==='radio'", block: {…}}
                    {exp: undefined, block: {…}}
                ]

                判断if数组的虚拟dom是不是静态标记,如果不是,那么则父节点也不是
             */
            if (node.ifConditions) { 
                for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
                    var block = node.ifConditions[i$1].block;  //虚拟dom
                    markStatic$1(block);
                    if (!block.static) {
                        node.static = false;
                    }
                }
            }
        }
    }
    

//判断是否是静态的ast虚拟dom type必须不等于2和3,pre必须为真
function isStatic(node) {
        debugger
        if (node.type === 2) { // node是一个表达式的话,直接就标记成【非静态】节点
            return false
        }
        if (node.type === 3) { // text 文本节点或者是空注释节点 【静态】
            return true
        }
        // 如果既不是表达式也不是文本节点,就说明这是一个标签,有子节点,就根据这个标签上的一些属性
        // 或者标签名等判断是不是一个静态节点。
        return !!( // 其它节点,如'div'标签
            node.pre ||   //标记 标签是否还有 v-pre 指令 ,如果有则为真
            (
                !node.hasBindings && // 没有动态标记元素
                !node.if && !node.for && // 没有 v-if 或者 v-for 或者 v-else
                !isBuiltInTag(node.tag) && // 没有 slot,component
                isPlatformReservedTag(node.tag) && // 是保留标签,html 原有的标签 或者svg标签
                //判断当前ast 虚拟dom 的父标签 如果不是template则返回false,如果含有v-for则返回true
                !isDirectChildOfTemplateFor(node) && 
                //node的key必须每一项都符type,tag,attrsList,attrsMap,plain,parent等的字符串
                Object.keys(node).every(isStaticKey) 
            )
        )
}

6-2 markStaticRoots

作用:

第二步,标记标记静态根,逻辑和第一步差不多

源码:

function markStaticRoots(node, isInFor) {
        debugger
        if (node.type === 1) {
            if (
                node.static || //静态节点
                node.once // v-once 只渲染一次节点。
            ) {
                node.staticInFor = isInFor;
            }

            if (
                node.static &&  //如果是静态节点
                node.children.length && //如果是有子节点
                !(
                    node.children.length === 1 && //如果只有一个子节点
                    node.children[0].type === 3 //文本节点
                )) {
                node.staticRoot = true; //标记静态根节点
                return
            } else {
                node.staticRoot = false;
            }
            if (node.children) {
                for (var i = 0, l = node.children.length; i < l; i++) {
                    markStaticRoots(
                        node.children[i],
                        isInFor || !!node.for
                    );
                }
            }
            if (node.ifConditions) {
                for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
                    markStaticRoots(
                        node.ifConditions[i$1].block,
                        isInFor
                    );
                }
            }
        }
    }

最后,我们看一下,打完标记的虚拟dom:

image.png

7. generate

7-1. 基本信息

作用:

generate,生成render表达式。

经过前面对AST进行了优化之后,需要将整个AST变成一个可执行的代码块,也就是render函数。于是模板编译器使用了generate对AST进行了代码生成。

源码:

var createCompiler = createCompilerCreator(
function baseCompile(
            template, // "<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
            options // 这里已经baseOptions和options的合并项了
        ) {
            var ast = parse(template.trim(), options);
            if (options.optimize !== false) {  
                // 循环递归虚拟node,标记是不是静态节点
                // 根据node.static或者 node.once 标记staticRoot的状态
                optimize(ast, options);
            }
            var code = generate(ast, options);
            return {
                ast: ast,
                render: code.render, //code 虚拟dom需要渲染的参数函数
                staticRenderFns: code.staticRenderFns  //空数组
            }
}

返回的code为下面的格式,他分为渲染函数render和静态渲染函数staticRenderFns

{ 
    render: "with(this){return _c('div',{attrs:{\"id\":\"app\"}},
    [_c('div',[_v(\"我是静态节点\")]),_v(\" \"),(showDom)?_m(0):_e()])}",
    
    staticRenderFns: ["with(this){return _c('div',[_v(\"我只渲染一次\")])}"]
}

generate

    function generate(
        ast,
        options
    ) {
        
        // 生成状态
        // *  扩展指令,on,bind,cloak,方法
        // *  dataGenFns 获取到一个数组,数组中有两个函数genData和genData$1
        var state = new CodegenState(options);
        /**
         * 根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,
         * 或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
         * code,例:"_c('div',{attrs:{"id":"app"}},[_c('div',[_v("我是静态节点")]),_v(" "),(showDom)?_m(0):_e()])"
         */
        debugger        
        var code = ast ? genElement(ast, state) : '_c("div")';
        debugger
        return {
            //with 绑定js的this 缩写
            render: ("with(this){return " + code + "}"),
            staticRenderFns: state.staticRenderFns //空数组
        }
    }

先看下生成的state,dataGenFns是个数组,数组中有两个函数genData和genData$1

image.png

7-2. CodegenState

作用:

扩展指令,on,bind,cloak,方法,dataGenFns 获取到一个数组,数组中有两个函数genData和genData$1

源码:

   var CodegenState = function CodegenState(
        options // 基础配置
        ) {
        this.options = options;
        this.warn = options.warn || baseWarn; //警告日志输出函数
        // 拿到数组中,所有key为transformCode的对象项组成的数组  []
        this.transforms = pluckModuleFunction(options.modules, 'transformCode');
        //获取到一个数组,数组中有两个函数genData和genData$1   [ƒ genData(el), ƒ genData$1(el)]
        this.dataGenFns = pluckModuleFunction(options.modules, 'genData');

        // 扩展指令,on,bind,cloak,方法
        this.directives = extend(
            extend(
                {},
                baseDirectives
            ),
            options.directives
        );
        //保留标签 判断是不是真的是 html 原有的标签 或者svg标签
        var isReservedTag = options.isReservedTag || no; 
        //不是原生标签就也许是组件
        this.maybeComponent = function (el) {
            return !isReservedTag(el.tag); // 原生标签
        };
        this.onceId = 0;
        //静态渲染方法
        this.staticRenderFns = [];
    };

7-3. genElement

作用:

genElement的逻辑先对当前元素的属性部分进行了代码生成,比如v-if、v-for等。 之后又对元素的body部分进行了代码块生成

源码:

    function genElement(
        el, //ast对象或者虚拟dom
        state //渲染虚拟dom的一些方法
    ) {
        debugger
        if (el.staticRoot && !el.staticProcessed) {
            //将子节点导出虚拟dom 渲染函数的参数形式。静态渲染
            return genStatic(el, state)
        } else if (el.once && !el.onceProcessed) {
            //参考文档 https://cn.vuejs.org/v2/api/#v-once
            // v-once
            // 不需要表达式
            // 详细:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能
            // <!-- 单个元素 -->
            // <span v-once>This will never change: {{msg}}</span>
            return genOnce(el, state);
        } else if (el.for && !el.forProcessed) {
            // v-for
            //判断标签是否含有v-for属性 解析v-for指令中的参数 并且返回 虚拟dom需要的参数js渲染函数
            return genFor(el, state)
        } else if (el.if && !el.ifProcessed) { //判断标签是否有if属性
            // v-if
            //判断标签是否含有if属性 解析 if指令中的参数 并且返回 虚拟dom需要的参数js渲染函数
            return genIf(el, state)
        } else if (el.tag === 'template' && !el.slotTarget) {
            //标签是模板template
            //获取虚拟dom子节点
            return genChildren(el, state) || 'void 0'
        } else if (el.tag === 'slot') {
            //如果标签是插槽
            return genSlot(el, state)
        } else {
            // component or element
            //组件或元素
            var code;
            if (el.component) { //如果是组件

                //创建一个虚拟dom 的参数渲染的函数
                code = genComponent(
                    el.component,
                    el,
                    state
                );
            } else {

                var data = el.plain ?  //如果标签中没有属性则这个标志为真
                    undefined :
                    genData$2(el, state);

                var children = el.inlineTemplate ? //是不是内联模板标签
                    null :
                    genChildren(el, state, true);
                code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
            }

            // module transforms
            for (var i = 0; i < state.transforms.length; i++) {
                code = state.transforms[i](el, code);
            }
            //返回 虚拟dom需要的参数js渲染函数
            return code
        }
    }

7-4. genStatic

作用:

打静态标记

源码:

    function genStatic(el, state) {
        debugger
        //标记已经静态处理过
        el.staticProcessed = true;
        //添加渲染函数
        // 由于上面设置el.staticProcessed为true,这时候递归调用genElement,则会进入
        // 判断的else if里面
        state.staticRenderFns.push(("with(this){return " + (genElement(el, state)) + "}"));

        //返回虚拟dom渲染需要的参数格式
        return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
    }

7-5. genOnce

作用:

源码:

    function genOnce(el, state) {
        //标志已经处理过的
        el.onceProcessed = true;
        //【说明1】
        if (el.if && !el.ifProcessed) {
            //判断标签是否含有if属性
            return genIf(el, state)
        } else if (el.staticInFor) {
            var key = '';
            
            var parent = el.parent;
            while (parent) {
                if (parent.for) {
                    key = parent.key;
                    break
                }
                parent = parent.parent;
            }
            if (!key) {
                "development" !== 'production' && state.warn(
                    "v-once can only be used inside v-for that is keyed. "
                );
                //genElement根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
                return genElement(el, state)
            }
            //genElement根据el判断是否是组件,或者是否含有v-once,v-if,v-for,是否有template属性,或者是slot插槽,转换style,css等转换成虚拟dom需要渲染的参数函数
            return ("_o(" + (genElement(el, state)) + "," + (state.onceId++) + "," + key + ")")
        } else {
            //将子节点导出虚拟dom 渲染函数的参数形式
            return genStatic(el, state)
        }
    }

【说明1】,el.if ,el.if定义在function processIf函数中

    //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
    function processIf(el) {
        var exp = getAndRemoveAttr(el, 'v-if'); //获取v-if属性
        if (exp) {
        // <div v-if='showDom'>我只渲染一次</div>,这里拿到的el.if就是'showDom'
            el.if = exp; 
            addIfCondition(el, {  //为if指令添加标记
                exp: exp,
                block: el
            });
        } else {
            if (getAndRemoveAttr(el, 'v-else') != null) {
                el.else = true;
            }
            var elseif = getAndRemoveAttr(el, 'v-else-if');
            if (elseif) {
                el.elseif = elseif;
            }
        }
    }

7-6. genIf

作用:

解析 if指令中的参数 如果存在,会返回一个三元表达式,这里就是v-if的源码了

源码:

function genIf(
        el, 
        state, 
        altGen,
        altEmpty
    ) {
        el.ifProcessed = true; // avoid recursion 标记已经处理过 避免递归
        //el.ifConditions.slice() if条件参数
        //解析 if指令中的参数 并且返回 虚拟dom需要的参数js渲染函数
        return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions(
        conditions, //[{exp: view中的if属性,block: el当前渲染的虚拟组件}] 例:[{exp: "showDom", block: {…}}]
        state,  // CodegenState 如function generate中通过new CodegenState(options)生成的 例:{dataGenFns: (2) [ƒ, ƒ]...}
        altGen,  // undefined 没看到哪里有入参
        altEmpty // undefined 没看到哪里有入参
    ) {
        debugger
        if (!conditions.length) { //如果conditions 不存在 则返回一个空的虚拟dom参数
            return altEmpty || '_e()'
        }
        var condition = conditions.shift();  //取第一项
        /**
         * 判断if指令参数是否存在 
         * 如果存在则递归condition.block 数据此时ifProcessed 变为true 下次不会再进来
         * 
         * 
         */
        if (condition.exp) {  
            /**
             * genTernaryExp(condition.block) 例: "_m(0)"
             * genIfConditions(conditions, state, altGen, altEmpty) 例:"_e()"
             * conditions为删除了第一项后剩余的数组项
             *  最终表达式:(showDom)?_m(0):_e()
             */
            return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
        } else {
            return ("" + (genTernaryExp(condition.block))) //没有表达式直接生成元素 像v-else
        }
        
    //如果用v-once生成像(a)?_m(0):_m(1)这样的代码
    function genTernaryExp(el) {
            //数据此时ifProcessed 变为true 下次不会再进来
            return altGen ?
                altGen(el, state)  //altGen 一个自定义函数吧
                : el.once ?     //静态标签标志 存在么 不存在
                    genOnce(el, state)  //导出一个静态标签的虚拟dom参数
                    : genElement(el, state) //递归el 数据此时ifProcessed 变为true 下次不会再进来
    }
}