vue源码分析【1】-new Vue之前

105 篇文章 0 订阅
12 篇文章 0 订阅

当前篇:vue源码分析【1】-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>
    </div>

    <script>
        debugger;
        var app = new Vue({
            el: '#app',
            beforeCreate() { },
            created() { },
            beforeMount() { },
            mounted: () => { },
            beforeUpdate() { },
            updated() { },
            beforeDestroy() { },
            destroyed() { },
            data: function () {
                return {
                    money: 100,
                    num: 12,
                    arryList: [{name:'子树'}]
                }
            },
            computed: {
                total() {
                    return this.money * this.num;
                }
            },
            methods: {
                getMoreMoney() {
                    this.money = this.money * 2
                    this.arryList.unshift({name: '大树'})
                }
            }
        })
    </script>
</body>
</html>

1. 前言

此篇文章旨在说明new Vue之前做了哪些操作,给Vue构造函数挂载了哪些方法和属性,由于没有进入new Vue,所以不会执行挂载函数,而仅仅是挂载操作。另外,在new Vue之前的代码,意味着此时都不会拿到html中new Vue的入参el,beforeCreate等等。

因此,此文章最大的目的是作为后续文章的参考,注意,由于挂载函数不执行,所以我们在讲解挂载函数时会省略一些代码,详细代码将在后续文章中逐步讲解。

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

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

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


2. 整体流程

// 定义 Vue.prototype._init 方法
initMixin(Vue)
/**
 * 定义:
 *   Vue.prototype.$data
 *   Vue.prototype.$props
 *   Vue.prototype.$set
 *   Vue.prototype.$delete
 *   Vue.prototype.$watch
 */
stateMixin(Vue)
/**
 * 定义 事件相关的 方法:
 *   Vue.prototype.$on
 *   Vue.prototype.$once
 *   Vue.prototype.$off
 *   Vue.prototype.$emit
 */
eventsMixin(Vue)
/**
 * 定义:
 *   Vue.prototype._update
 *   Vue.prototype.$forceUpdate
 *   Vue.prototype.$destroy
 */
lifecycleMixin(Vue)
/**
 * 执行 installRenderHelpers,在 Vue.prototype 对象上安装运行时便利程序
 * 
 * 定义:
 *   Vue.prototype.$nextTick
 *   Vue.prototype._render
 */
renderMixin(Vue)

3. initMixin

作用:

初始化vue,挂载_init方法。

在Vue原型上挂载一个_init方法,这一步只是挂载方法,并没有执行

源码:

//初始化vue
initMixin(Vue);    
var uid$3 = 0;

function initMixin(Vue) {
        Vue.prototype._init = function (options) { //初始化函数
        ...
}
console.log(Vue.prototype)

执行完initMixin,我们看下此时Vue的原型,ok,符合预期,就只挂载了一个_init方法:

{
    _init: ƒ (options),
    constructor: ƒ Vue(options), // 此项是默认的
    __proto__: Object // 此是默认的
}

4. stateMixin

4-1. 基本信息

作用:

数据绑定,挂载 d a t a , data, dataprops, s e t , set, setdelete,$watch方法

流程:

  • 分别定义dataDef对象和propsDef对象,给它们各自挂载一个get方法,分别返回this._data和 this._props
  • 再给dataDef和propsDef分别挂载一个set方法,当改变数据时发出警告。
    • dataDef的set规定:不能直接替换实例根$data
    • propsDef的se规定:不能更改props的值
  • 通过Object.defineProperty给Vue原型挂载对应data和props的get和set方法。
  • 为确保更新数据或者删除数据更更新视图,挂载 s e t 和 set和 setdelete
  • 挂载$watch

源码:

stateMixin(Vue);
function stateMixin(Vue) {
        debugger
        //直接声明的定义对象生成的流在某种程度上有问题
        //在使用Object.defineProperty时,我们必须循序渐进地进行构建
        //  定义一个data对象
        var dataDef = {};
        //重新定义get 和set方法
        dataDef.get = function () {
            return this._data //获取data中的数据
        };
        //  定义一个props对象
        var propsDef = {};
        propsDef.get = function () {
            return this._props// 获取props 数据
        };
        {
            dataDef.set = function (newData) {
                // 修改数据时,避免直接替换实例根$data。
                /**
                 * 以下直接给根$data赋值会警告:
                 *  const list={
                        name:"子树"
                    }
                    this.$data = list;

                    正确的写法:
                    const list={
                        name:"子树"
                    }
                    Object.assign(this.$data,list); 
                 * 
                 */
                warn(
                    'Avoid replacing instance root $data. ' +
                    'Use nested data properties instead.',
                    this
                );
            };
            propsDef.set = function () {
                /**
                 * props 是只读数据,不能更改。
                 * 这里也属于Vue单向数据流的范畴,如父组件能通过props改变子组件,
                 * 但子组件改变传入的props就会警告,只允许单向流动
                 */
                warn("$props is readonly.", this);
            };
        }

        // 数据响应式的关键方法
        // 【说明1】
        Object.defineProperty(Vue.prototype, '$data', dataDef);
        Object.defineProperty(Vue.prototype, '$props', propsDef);

        /** 【说明2】
         * 确保设置值能更新视图
         * 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
         * 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知
        */
        Vue.prototype.$set = set;
        /**
         * 确保删除能更新视图
         * 通过 Vue.delete 或者 vm.$delete 删除 target 对象的指定 key
         * 数组通过 splice 方法实现,对象则通过 delete 运算符删除指定 key,并执行依赖通知
        */
        Vue.prototype.$delete = del;

        /**
         *  挂载$watcher,返回 unwatch函数:
            创建 watcher 实例,如果设置了 immediate,则立即执行一次 cb
         *  返回 unwatch
         *  unwatch 函数: 用于取消 watch 监听
         */
        Vue.prototype.$watch = function (
            expOrFn, //用户手动监听
            cb, // 监听 变化之后 回调函数
            options //参数
        ) {
            var vm = this;
            if (isPlainObject(cb)) { //判断是否是对象 如果是对象则递归 深层 监听 直到它不是一个对象的时候才会跳出递归
                //    转义handler 并且为数据 创建 Watcher 观察者
                return createWatcher(
                    vm,
                    expOrFn,
                    cb,
                    options
                )
            }
            options = options || {};
            options.user = true; //用户手动监听, 就是在 options 自定义的 watch

            //实例化Watcher 观察者
            var watcher = new Watcher(
                vm, //vm  vode
                expOrFn,  //函数 手动
                cb, //回调函数
                options  //参数
            );
            if (options.immediate) {
                //回调触发函数
                cb.call(vm, watcher.value);
            }
            return function unwatchFn() { //卸载观察者,解除监听
                //从所有依赖项的订阅方列表中删除self。
                watcher.teardown();
            }
        };
}

4-2 说明

4-2-1 说明1:Object.defineProperty()

Object.defineProperty(Vue.prototype, ‘$data’, dataDef)的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。通过get进行依赖收集,而每个set方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

Object.defineProperty(obj, prop, desc)

  • obj 需要定义属性的当前对象
  • prop 当前需要定义的属性名
  • desc 属性描述符
    一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性

同样,执行完这一行代码,我们再次打印Vue.prototype,如下:

{
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
    $data: undefined, // 本次加上的
    get $data: ƒ (), // 本次加上的
    set $data: ƒ (newData) // 本次加上的
}

4-2-2 说明2:Vue.prototype.$set = set

这个set方法如下:

function set(target, key, val) {...}

它做了如下动作:

  • 通过 Vue.set 或者 this.$set 方法给 target 的指定 key 设置值 val
  • 如果 target 是对象,并且 key 原本不存在,则为新 key 设置响应式,然后执行依赖通知

vue开发时经常会遇到当vue实例已经创建好了,有时候需要再次给数据赋值时,并不能在视图中改变。

比如:

<template>
  <div>
    测试
    {{ list }}
    {{ obj }}
    <Button @click="change">改变数组</Button>
    <Button @click="addValue">改变对象</Button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [1, 2, 3],
      obj: { name: "子树" },
    };
  },
  methods: {
    change() {
      this.list[1] = 666; // 通过下标直接改变数组
      console.log(this.list);
    },
    addValue() {
      this.obj.value = true; // 给对象添加一个属性
      console.log(this.obj);
    },
  },
};
</script>

显示如图:

image.png

我们会发现,控制台打印的数据已经更新了,但是,视图并没有同步更新。那么,我们怎么让视图同步更新呢?

通过Vue.set(target, key, val)来改变:

  • target: 要改变的对象或者数组
  • key: 数组的下标或者对象的key
  • val: 赋值的新的值
  methods: {
    change() {
    //   this.list[1] = 666;
      Vue.set(this.list, 1, 666)
      console.log(this.list);
    },
    addValue() {
    //   this.obj.value = true;
      Vue.set(this.obj, 'value', true)
      console.log(this.obj);
    },
  },

此时,我们再回到页面,可以看到控制台和页面都同步更新了


4-3. 结果

stateMixin(Vue)执行完后,我们再打印Vue.prototype结果如下:

{
    $delete: ƒ del(target, key), //本次加上的
    $set: ƒ (target, key, val),  //本次加上的
    $watch: ƒ ( expOrFn), //本次加上的
    $data: undefined,  //本次加上的
    $props: undefined, //本次加上的
    get $data: ƒ (),  //本次加上的
    set $data: ƒ (newData),  //本次加上的
    get $props: ƒ (),  //本次加上的
    set $props: ƒ (),  //本次加上的
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
}

5. eventsMixin

5-1. 基本信息

作用:

初始化事件绑定方法

流程:

  • 依次挂载$on,$once,$off,$emit事件相关的属性。

源码:

// 以下各个挂载事件将在各个小节讲解
function eventsMixin(Vue) {
        var hookRE = /^hook:/;  //开头是hook: 的字符串
        
        /**
         * 监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm
         */
         Vue.prototype.$on = function (){...}
         
         /**
         * 监听Vue自定义事件,返回Vm。
         * once比较特殊,只触发一次,然后就会移除监听器
         */
         Vue.prototype.$once = function (event, fn) {...}
         
         /**
         * vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件
         */
          Vue.prototype.$off = function (event, fn) {...}
          
         /**
         *  触发事件
         */
         Vue.prototype.$emit = function (event) {...}
}

5-2. $on

作用:

监听Vue自定义事件,把所有事件拆分存放到_events 数组中,返回Vm

流程:

  • 如果传入的事件名event为数组,则遍历,递归调用$on
  • 否则,如果传入的事件名event为单个事件名,把所有事件拆分存放到_events 数组中,比如传入的eventclickfnaddNumvm._events['click']原本是[],则结果如下:vm._events = { click: [addNum] },然后返回vm
    • 如果事件名是以 hook: 开头的(如hook:mount=‘getNum’),则标记为vue系统内置钩子函数(比如vue 生命周期函数等)

源码:

Vue.prototype.$on = function (
            event, // 单个的事件名称或者有多个事件名组成的数组
            fn // 回调
            ) {
            var this$1 = this;
            var vm = this;
            // 【逻辑 1】 event为多个事件组成的数组
            if (Array.isArray(event)) {
                for (var i = 0, l = event.length; i < l; i++) {
                    // event为事件数组,则遍历,递归调用$on
                    this$1.$on(event[i], fn);
                }
            }
            // 【逻辑 2】 event为单个事件名
            else {
                /**
                 * 把所有事件拆分存放到_events 数组中
                 * vm._events本身是个对象,
                 * 比如传入的event为click,fn为addNum,vm._events['click']原本是[],则结果如下:
                 * vm._events = { click: [addNum] }
                 */
                (vm._events[event] || (vm._events[event] = [])).push(fn);
                /**
                 * 如果事件名是以 hook: 开头的(如hook:mount='getNum'),
                 * 标记为vue系统内置钩子函数, 比如vue 生命周期函数等
                 */
                if (hookRE.test(event)) {
                    vm._hasHookEvent = true;
                }
            }
            return vm
};

5-3. $once

作用:

监听Vue自定义事件,先把传入的事件在vm上解绑,然后执行传入的事件。

虽然最终也触发$on,但once比较特殊,只触发一次,然后就会移除监听器

流程:

  • 创建一个封装事件on,用于解绑和执行事件
  • 将传入的事件挂载到封装事件on上,然后调用vm.$on(event, on);
  • 接着就是$on的逻辑了,也就是说,$once最终还是会走到$on

源码:

        Vue.prototype.$once = function (event, fn) {
            var vm = this;
            function on() {
                // 解绑事件
                vm.$off(event, on);
                // 执行回调事件
                fn.apply(vm, arguments);
            }
            // 将传入的回调函数fn挂载到我们定义on函数的fn上。
            on.fn = fn;
            // 再把on添加到vm上
            vm.$on(event, on);
            return vm
        };


5-4. $off

作用:

vue把事件添加到一个数组队列里面,通过删除该数组事件队列,而达到解绑事件。

返回删除了_events属性中相应事件的vm。

流程:

  • 如果没有参数的情况下,移除所有监听器
  • 否则,如果传入的event是数组事件 则循环回调递归Vue.$off
  • 否则,vm._events中,不存在传入的事件名
  • 否则,如果回调函数不存在则清空 _events对应的事件名中所有事件
  • 否则,在事件cbs(vm._events中传入的event属性)数组中移除我们指定的回调函数

源码:

        Vue.prototype.$off = function (event, fn) {
            var this$1 = this;
            var vm = this;
            // 【逻辑 1】 如果没有参数的情况下,移除所有监听器
            if (!arguments.length) {
                // 赋值一个没有原型的空对象
                vm._events = Object.create(null);
                return vm
            }
            // 【逻辑 2】  如果传入的event是数组事件 则循环回调递归Vue.$off
            if (Array.isArray(event)) {
                for (var i = 0, l = event.length; i < l; i++) {
                    this$1.$off(event[i], fn);
                }
                return vm
            }
            //  【逻辑 3】,vm._events中,不存在传入的事件名
            var cbs = vm._events[event];
            if (!cbs) {
                return vm
            }
            // 【逻辑 4】,如果回调函数不存在则清空 _events对应的事件名中所有事件
            if (!fn) {
                vm._events[event] = null;
                return vm
            }
            // 【逻辑 5】在事件cbs(vm._events中传入的event属性)数组中移除我们指定的回调函数
            if (fn) {
                var cb;
                var i$1 = cbs.length;
                while (i$1--) {
                    // 拿到vm._events中event事件名下每个事件
                    cb = cbs[i$1];
                    // cb.fn为了兼容$once中on.fn = fn;
                    if (cb === fn || cb.fn === fn) {
                        cbs.splice(i$1, 1);
                        break
                    }
                }
            }
            return vm
        };

5-5. $emit

作用:

触发事件

流程:

  • 获取vm上传入的事件cbs,判断是否是数组,是数组则遍历cbs
  • 执行传入的事件名对应的事件

源码:

        Vue.prototype.$emit = function (event) {
            var vm = this;
            {
                var lowerCaseEvent = event.toLowerCase(); //转成小写 例:onClick=》onclick
                if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                    // 注意 html属性 是大小写敏感的, 你不能利用 v-on 来监听驼峰写法的事件, 在模板中
                    // 你应该使用 get-num 来替代 getNum

                    // 实际使用中这两者写法没有区别的,这个提示有点疑惑
                    tip(
                        "Event \"" + lowerCaseEvent + "\" is emitted in component " +
                        (formatComponentName(vm)) + " but the handler is registered for \"" + 
                        "Note that HTML attributes are case-insensitive and you cannot use " +
                        "v-on to listen to camelCase events when using in-DOM templates. " +
                        "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" 
                    );
                }
            }
            //获取传入的事件名的值
            var cbs = vm._events[event];
            if (cbs) {
                //如果长度大于1 将它变成一个真正的数组
                cbs = cbs.length > 1 ? toArray(cbs) : cbs;
                //将参数变成一个真正数组
                var args = toArray(arguments, 1);
                //循环事件
                for (var i = 0, l = cbs.length; i < l; i++) {
                    try {
                        //执行触发事件
                        cbs[i].apply(vm, args);
                    } catch (e) {
                        //如果发生错误则发出报错警告
                        handleError(e, vm, ("event handler for \"" + event + "\""));
                    }
                }
            }
            return vm
        };
    }

5-6. 结果

(Vue)执行完后,我们再打印Vue.prototype结果如下:

{
    $delete: ƒ del(target, key), 
    $set: ƒ (target, key, val), 
    $watch: ƒ ( expOrFn), 
    $data: undefined,  
    $props: undefined,
    get $data: ƒ (),  
    set $data: ƒ (newData), 
    get $props: ƒ (), 
    set $props: ƒ (),  
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
    $emit: ƒ (event) // 本次加上的
    $off: ƒ (event, fn) // 本次加上的
    $on: ƒ ( event, fn) // 本次加上的
    $once: ƒ (event, fn) // 本次加上的
}

6. lifecycleMixin

6-1. 基本信息

作用:

挂载初始化(_update), 更新 ($forceUpdate),销毁($destroy) 函数

流程:

  • 依次挂载_update,$forceUpdate,$destroy,方法属性

源码:

// 以下各个挂载事件将在各个小节讲解
function lifecycleMixin(Vue) {
        /**
         * 更新数据函数
         */
        Vue.prototype._update = function (vnode, hydrating) { ... }
        
         /**
         * 更新数据 观察者数据
         */
         Vue.prototype.$forceUpdate = function () { ... }
        
         /**
         * 销毁组建周期函数
         */
        Vue.prototype.$destroy = function () { ... }
}

6-2. _update

作用:

更新视图,更新数据函数,包括首次渲染和后续更新, 也是 patch 的入口

流程:

  • 通过判断vm上是否存在_vnode来执行初始化逻辑还是更新数据逻辑
  • 他们最终用到的都是patch函数

源码:

        Vue.prototype._update = function (vnode, hydrating) {
            var vm = this;
            // 是否 触发过 钩子Mounted
            // Todo
            if (vm._isMounted) {
                //触发更新数据 触发生命周期函数
                callHook(vm, 'beforeUpdate');
            }
            // 标志上一个 el 节点
            var prevEl = vm.$el;
            // 标志上一个 vonde
            var prevVnode = vm._vnode;  
            // 活动实例
            var prevActiveInstance = activeInstance;
            activeInstance = vm;
            //标志上一个 vonde
            vm._vnode = vnode; 
            // 【逻辑 1】 执行初始化
            //如果不存在表示上一次没有创建过vnode,即当前是初始化,第一次进来
            if (!prevVnode) { 
                // 这里通过patch函数,是创建真实dom
                // 注意,patch在vue中非常核心,我们将在后面用单独文章来解析
                // Todo
                vm.$el = vm.__patch__(
                    vm.$el, //真正的dom
                    vnode, //vnode
                    hydrating, // 空
                    false /* removeOnly */,
                    vm.$options._parentElm, //父节点 空
                    vm.$options._refElm //当前节点 空
                );
                //  初始补丁之后不需要ref节点,这可以防止在内存中保留分离的DOM树
                vm.$options._parentElm = vm.$options._refElm = null;
            } 
            // 【逻辑 2】 执行数据更新
            else { 
                // 如果这个prevVnode存在,表示vno的已经创建过,只是更新数据而已
                // 比较新旧节点,生成新的dom
                vm.$el = vm.__patch__(prevVnode, vnode);
            }
            // 更新全局的activeInstance原来的旧值
            activeInstance = prevActiveInstance; 
            //  更新vue参考
            if (prevEl) {
                prevEl.__vue__ = null;
            }
            if (vm.$el) { // 更新真实dom上对虚拟dom的指向
                vm.$el.__vue__ = vm;
            }
            // if parent is an HOC, update its $el as well
            if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
                vm.$parent.$el = vm.$el;
            }
            // updated hook is called by the scheduler to ensure that children are
            // updated in a parent's updated hook.
        };

6-3. $forceUpdate

作用:

vue强制更新,会更新视图和数据,触发updated生命周期

  • 使用场景:因为数据层次太多,render函数没有自动更新,需手动强制刷新。

  • 使用语法:直接调用 this.$forceUpdate()

源码:

Vue.prototype.$forceUpdate = function () {
            var vm = this;
            if (vm._watcher) {
                vm._watcher.update();
            }
};

6-4. $destroy

作用:

销毁实例,断开连接,卸载全部指令和事件监听器。

流程:

  • 未销毁的实例才会执行销毁逻辑
  • 如果父节点没有要销毁,才从父节点上移除自身
  • 卸载依赖_watcher,队列_watchers,删除引用
  • 打上销毁标记destroyed,执行destroyed钩子,销毁实例
  • 通过__patch__函数销毁节点
  • 关闭事件监听器,删除__vue__ 参数

源码:

        Vue.prototype.$destroy = function () {
            var vm = this;
            // 如果是已经销毁过退出
            if (vm._isBeingDestroyed) {
                return
            }
            callHook(vm, 'beforeDestroy');
            // 进入这个函数,执行销毁事件,需要更新销毁标记
            vm._isBeingDestroyed = true;
            var parent = vm.$parent;
            // 从父节点的$children移除自身
            if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
                remove(parent.$children, vm);
            }
            // 卸载依赖监听
            if (vm._watcher) {
                vm._watcher.teardown();
            }
            // 卸载依赖队列的监听
            var i = vm._watchers.length;
            while (i--) {
                vm._watchers[i].teardown();
            }

            //从数据ob中删除引用
            // 被冻结的对象可能没有观察者。
            if (vm._data.__ob__) {
                vm._data.__ob__.vmCount--;
            }

            // 调用最后一个钩子
            vm._isDestroyed = true;
            // 调用 __patch__,销毁节点
            vm.__patch__(vm._vnode, null);
            // 销毁实例
            callHook(vm, 'destroyed');
            // 关闭事件监听器
            vm.$off();
            // 删除vue 参数
            if (vm.$el) {
                vm.$el.__vue__ = null;
            }
            // 释放循环引用 销毁父节点
            if (vm.$vnode) {
                vm.$vnode.parent = null;
            }
        };

6-5. 结果

此时Vue.prototype为:

{
    $delete: ƒ del(target, key), 
    $set: ƒ (target, key, val), 
    $watch: ƒ ( expOrFn), 
    $data: undefined,  
    $props: undefined,
    get $data: ƒ (),  
    set $data: ƒ (newData), 
    get $props: ƒ (), 
    set $props: ƒ (),  
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
    $emit: ƒ (event), 
    $off: ƒ (event, fn),
    $on: ƒ ( event, fn),
    $once: ƒ (event, fn),
    $destroy: ƒ (), // 本次加上去的
    $forceUpdate: ƒ (), // 本次加上去的
    _update: ƒ (), // 本次加上去的
}

7. renderMixin

7-1. 基本信息

作用:

挂载_render,生成 VNode

挂载_$nextTick函数,将回调延迟到下次 DOM 更新循环之后执行

流程:

  • 依次挂载_render,_$nextTick,方法属性

源码:

// 以下各个挂载事件将在各个小节讲解
function renderMixin(Vue) {
        // 在实例上安装渲染工具函数
        installRenderHelpers(Vue.prototype);

        Vue.prototype.$nextTick = function (fn) { ... }

        Vue.prototype._render = function () { ... }
}

7-2. installRenderHelpers

作用:

在实例上安装渲染工具函数

流程:

将一系列工具函数,通过简写属性key挂载到实例上。例如:target._o = markOnce

源码:

    function installRenderHelpers(target) {

        target._o = markOnce; //实际上,这意味着使用唯一键将节点标记为静态。* 标志 v-once. 指令
        target._n = toNumber; //字符串转数字,如果失败则返回字符串
        target._s = toString; // 将对象或者其他基本数据 变成一个 字符串
        target._l = renderList; //根据value 判断是数字,数组,对象,字符串,循环渲染
        target._t = renderSlot; //用于呈现<slot>的运行时帮助程序 创建虚拟slot vonde
        target._q = looseEqual; //检测a和b的数据类型,是否是不是数组或者对象,
                                //对象的key长度一样即可,数组长度一样即可
                                
        target._i = looseIndexOf; //或者 arr数组中的对象,或者对象数组 是否和val 相等
        target._m = renderStatic;//用于呈现静态树的运行时助手。 创建静态虚拟vnode
        target._f = resolveFilter; // 用于解析过滤器的运行时助手
        target._k = checkKeyCodes; // 检查两个key是否相等,如果不想等返回true 如果相等返回false
        target._b = bindObjectProps; //用于将v-bind="object"合并到VNode的数据中的运行时助手。  
                                     //检查value 是否是对象,并且为value 添加update 事件
                                     
        target._v = createTextVNode; //创建一个文本节点 vonde
        target._e = createEmptyVNode;  // 创建一个节点 为注释节点 空的vnode
        target._u = resolveScopedSlots; //  解决范围槽 把对象数组事件分解成 对象
        target._g = bindObjectListeners; //判断value 是否是对象,并且为数据 data.on 合并data和value 的on 事件
    }

7-3. $nextTick

作用:

回调延迟到下次 DOM 更新循环之后执行

使用场景:

比如我们现在需要获取接口数据去渲染一个列表,并且把列表的第一项设置为高亮。

 mounted: function () {
    this.loadData()
 },
 methods: {
    loadData () {
      getList.then(res => {
        this.list = res.data
        // 获取list的第一个节点,高亮
        this.$refs.list.getElementsByTagName('li')[0].style.color = 'blue'
      })
    },
  }

此时就会遇到个问题,我们获取节点li的时候,DOM并没有更新完成,ul下面还没有li节点,这时就会报错。此时就需要使用$nextTick方法,在DOM更新之后才执行回调。

如下,改动mounted:

  mounted: function () {
    this.$nextTick(() => {
        this.loadData()
    })
 },
 methods: {...}

流程:

为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数

源码:

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

function nextTick(
        cb,  // 回调函数
        ctx  // this的指向
        ) {
        var _resolve;
        // 添加当前回调函数到回调函数队列里面去,等待适当的时机执行
        callbacks.push(function () {
            if (cb) {
                // 如果cb存在 并且是一个函数就执行
                try {
                    cb.call(ctx);
                } catch (e) {
                    // 如果不是函数则报错
                    handleError(e, ctx, 'nextTick');
                }
            } else if (_resolve) {
                //_resolve 如果存在则执行
                _resolve(ctx);
            }
        });
        // 默认false
        if (!pending) {
            pending = true;
            //执行异步宏任务
            if (useMacroTask) {
                macroTimerFunc();
            } else {
                microTimerFunc();
            }
        }
        if (!cb && typeof Promise !== 'undefined') {
            //如果回调函数不存在 则声明一个Promise 函数
            return new Promise(function (resolve) {
                _resolve = resolve;
            })
        }
}

7-4. _render

作用:

渲染函数,生成vnod。

流程:

  • 遍历vm.$slots,设置所有属性中_rendered属性值为false
  • 设置vm.$scopedSlots
  • 执行render函数生成vnode,如果执行失败,则返回错误渲染结果,或以前的vnode,以防止渲染错误导致空白组件
  • 如果render函数生成的不是Vnode,则返回空的vnode

源码:

        Vue.prototype._render = function () {
            var vm = this;
            var ref = vm.$options;
            var render = ref.render;
            var _parentVnode = ref._parentVnode;

            //重置槽上的_render标记,以检查重复槽
            {
                for (var key in vm.$slots) {
                    //标志位
                    vm.$slots[key]._rendered = false;
                }
            }

            if (_parentVnode) { 
                // 获取插槽
                vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
            }
            //设置父vnode。这允许呈现函数具有访问权限,拿到占位符节点上的数据。
            vm.$vnode = _parentVnode;
            var vnode;
            try {
                // 创建一个空的组件
                // vm.$options.render = createEmptyVNode;
                // _renderProxy 代理拦截
                vnode = render.call(
                    vm._renderProxy, 
                    vm.$createElement
                );
            } catch (e) { //收集错误信息 并抛出
                handleError(e, vm, "render");
                // 返回错误渲染结果,或以前的vnode,以防止渲染错误导致空白组件
                {
                    if (vm.$options.renderError) {
                        try {
                            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
                        } catch (e) {
                            handleError(e, vm, "renderError");
                            vnode = vm._vnode;
                        }
                    } else {
                        vnode = vm._vnode;
                    }
                }
            }
            // 如果render函数生成的不是Vnode,则返回空的vnode
            if (!(vnode instanceof VNode)) {
                if ("development" !== 'production' && Array.isArray(vnode)) {
                    warn(
                        'Multiple root nodes returned from render function. Render function ' +
                        'should return a single root node.',
                        vm
                    );
                }
                vnode = createEmptyVNode();
            }
            vnode.parent = _parentVnode; 
            return vnode
        };

7-5. 结果

{
    $delete: ƒ del(target, key), 
    $set: ƒ (target, key, val), 
    $watch: ƒ ( expOrFn), 
    $data: undefined,  
    $props: undefined,
    get $data: ƒ (),  
    set $data: ƒ (newData), 
    get $props: ƒ (), 
    set $props: ƒ (),  
    _init: ƒ (options),
    constructor: ƒ Vue(options),
    __proto__: Object,
    $emit: ƒ (event), 
    $off: ƒ (event, fn),
    $on: ƒ ( event, fn),
    $once: ƒ (event, fn),
    $destroy: ƒ (),
    $forceUpdate: ƒ (),
    _update: ƒ (),
    $nextTick: ƒ (fn), // 本次加上去的
    _render: ƒ (), // 本次加上去的
    _b: ƒ bindObjectProps(), // 本次加上去的
    _e: ƒ (text), // 本次加上去的
    _f: ƒ resolveFilter(id), // 本次加上去的
    _g: ƒ bindObjectListeners(data, value, // 本次加上去的
    _i: ƒ looseIndexOf(arr, val), // 本次加上去的
    _k: ƒ checkKeyCodes(), // 本次加上去的
    _l: ƒ renderList(), // 本次加上去的
    _m: ƒ renderStatic(), // 本次加上去的
    _n: ƒ toNumber(val), // 本次加上去的
    _o: ƒ markOnce(tree, index, key), // 本次加上去的
    _q: ƒ looseEqual(a, b), // 本次加上去的
    _s: ƒ toString(val), // 本次加上去的
    _t: ƒ renderSlot(), // 本次加上去的
    _u: ƒ resolveScopedSlots(fns), // 本次加上去的
    _v: ƒ createTextVNode(val), // 本次加上去的
}

8. KeepAlive

8-1. 基本信息

作用:

kee-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存。

在平常开发中,有部分组件没有必要多次初始化,这时,我们需要将组件进行持久化,使组件的状态维持不变,在下一次展示时,也不会进行重新初始化组件。

使用场景:

如列表页面,点击第二页,选择一项进入详情。然后从详情页面返回列表页面时,打开的依然是第二页,且滚动条的位置也不会变。

//只有路径匹配到的 name 为 list 组件会被缓存
<keep-alive include="list">
    <router-view></router-view>
</keep-alive>

流程:

  • KeepAlive :以组件的格式定义内置组件的属性
  • builtInComponents:内置保存状态的组件

源码:

var patternTypes = [String, RegExp, Array]; //类型
// KeepAlive的属性就这些了
var KeepAlive = {
    name: 'keep-alive',
    abstract: true,
    props:{
            include: patternTypes,  // 类型允许[String, RegExp, Array]  缓存匹配的组件
            exclude: patternTypes, //  类型允许[String, RegExp, Array]  不缓存匹配的组件
            max: [String, Number] //   类型允许 [String, Number] 缓存组件的最大值
    },
    created: function created() {
            this.cache = Object.create(null); //创建一个缓存的空对象
            this.keys = []; // 缓存key数组
    },
    destroyed: function destroyed() {
            var this$1 = this;
            for (var key in this$1.cache) {
                // 销毁所有组件 【说明名1】
                pruneCacheEntry(
                    this$1.cache, key,
                    this$1.keys
                );
            }
    },
    mounted: function mounted() {...},
    render: function render() {...}
}
var builtInComponents = {
    KeepAlive: KeepAlive
}

【说明1 pruneCacheEntry】:

检测缓存中的组件,如果不是当前激活的组件则销毁

    function pruneCacheEntry(
        cache, // 缓存对象
        key, // 单个key  缓存对象this.cache中的key
        keys, // 多个key
        current //当前虚拟dom或者空
    ) {
        var cached$$1 = cache[key]; 
        if (cached$$1 && (!current || cached$$1.tag !== current.tag)) {
            // 如果exclude中的值不是当前组件(当前激活组件是不能销毁的),则让他销毁
            cached$$1.componentInstance.$destroy();
        }

        // 置空,移除key所在的对象和数组
        cache[key] = null;
        remove(keys, key);
    }

8-2. mounted

作用:

组件初始化 生命周期

流程:

  • 监听include,缓存其中的组件
  • 监听exclude,删除其中的组件

源码:

mounted: function mounted() { //组件初始化 生命周期
            var this$1 = this;
            this.$watch(
                'include',  //监听 include 数据是否有变化
                function (val) { //监听为完后更新的值
                    pruneCache(
                        this$1,
                        function (name) {
                            // 判断include属性中name是否存在  如:include:['a','b'], name: 'a',此时返回true
                            return matches(val, name);
                        });
                });
            this.$watch(
                'exclude',  //监听 exclude 数据是否有变化
                function (val) {
                    pruneCache(
                        this$1, 
                        function (name) {
                        //如果exclude 对象中存在name 不存在了 就 调用 检测缓存中的组件,如果不是当前激活的组件则销毁
                        return !matches(val, name);
                    });
                });
        },

【说明1 matches】

判断pattern 中是否还有 name

    function matches(pattern, name) {
        if (Array.isArray(pattern)) { //如果是数组
            return pattern.indexOf(name) > -1 // 是否存在
        } else if (typeof pattern === 'string') { //如果是字符串
            return pattern.split(',').indexOf(name) > -1 //判断是否存在
        } else if (isRegExp(pattern)) { // 如果是正则 则用正则表示
            return pattern.test(name)
        }
        return false
    }

【说明2 pruneCache】

删除缓存,销毁exclude中不是当前激活的组件

    function pruneCache(
        keepAliveInstance, //当前保持活动的实例
        filter // 函数过滤器
    ) {
        var cache = keepAliveInstance.cache; // 缓存对象
        var keys = keepAliveInstance.keys; // key数组
        var _vnode = keepAliveInstance._vnode;
        for (var key in cache) { 
            var cachedNode = cache[key]; // 获取缓存对象的值
            if (cachedNode) { 
                var name = getComponentName(cachedNode.componentOptions); // 获取组件的名称
                /**
                 * 例: include的情况下:
                 *      name为 'article',
                 *      filter(name)为 matches(['article','articleList'], 'article'),
                 *      则filter(name)为true,!filter(name)为false,即不执行
                 *      也就是说,当是include时,获取组件的名称在include的值中,就不删除
                 *      当是exclude时,获取组件的名称在exclude的值中,则执行相反操作,删除
                 */
                if (name && !filter(name)) { //如果name已经被销毁掉
                    pruneCacheEntry(
                        cache,
                        key,
                        keys,
                        _vnode
                    );
                }
            }
        }
    }

8-3. render

作用:

渲染 keepAlive 组件

流程:

  • 获取组件参数vnode.componentOptions
  • 如果在exclude中或者不在include,则直接返回vnode
  • 判断cache[key]是否存在,存在就把key放到keys的末尾
  • 不存在就添加缓存

源码:

        // 渲染 keepAlive 组件
        render: function render() {
            var slot = this.$slots.default; //获取插槽
            var vnode = getFirstComponentChild(slot); // 获取插槽子组件
            var componentOptions = vnode && vnode.componentOptions; //获取组件参数
            if (componentOptions) {
                var name = getComponentName(componentOptions); //获取组件名称
                var ref = this;
                var include = ref.include;
                var exclude = ref.exclude;
                // exclude优先于include
                if (
                    /**
                     * 如果name不存在include中,或者name存在exclude中,则进if
                     * 例: include为:['a','b'],name为'a'  false
                     *      exclude为:['a','b','c']     true
                     */
                    (include && (!name || !matches(include, name))) || 
                    // 如果exclude存在 并且name存在 并且name存在exclude对象中
                    (exclude && name && matches(exclude, name))
                ) {
                    return vnode  //返回虚拟dom
                }

                var ref$1 = this;
                var cache = ref$1.cache; //缓存的对象
                var keys = ref$1.keys; //获取keys 所有的key
                //同一个构造函数可以注册为不同的本地组件
                //单靠cid是不够的(#3269)
                //这里三目是 判断组件是否有cid 如果有 则 判断 是否有组件标签,如果有组件标签则返回 '::'+组件标签,
                //如果没有组件标签则返回空。如果没有 判断组件是否有cid 则返回 vnode.key
                var key = vnode.key == null 
                    ? componentOptions.Ctor.cid + (componentOptions.tag ?
                        ("::" + (componentOptions.tag)) :
                        '') :
                    vnode.key;

                if (cache[key]) {
                    vnode.componentInstance = cache[key].componentInstance; //直接获取组件实例化
                    remove(keys, key);
                    keys.push(key);
                } else {
                    //将虚拟dom缓存起来
                    cache[key] = vnode;
                    keys.push(key); //key缓存起来
                    //删除最老的条目
                    //设定最大的缓存值
                    if (this.max && keys.length > parseInt(this.max)) {
                        pruneCacheEntry(
                            cache,
                            keys[0], //第一个key
                            keys, //keys[]
                            this._vnode //当前活跃的组件
                        );
                    }
                }

                vnode.data.keepAlive = true;
            }
            return vnode || (slot && slot[0])
        }

9. initGlobalAPI(Vue)

9-1. 基本信息

作用:

初始化全局api 并且暴露 一些静态方法

流程:

  • 定义一个配置对象configDef,
  • 通过Object.defineProperty监听的属性config,获取的时候,获取到的是config配置对象。如果对这个config对象直接做变更, 就会提示“不要替换vue.config对象,而是设置单个字段”。
    • vue不希望我们直接去替换和变更整个config对象,如果有需要,希望去直接修改我们需要修改的值
  • 接着在Vue构造函数上挂载util,set,delete,nextTick方法
  • 然后遍历指令集合,在Vue.options挂载相应指令,然后设置_base属性值为Vue
  • 合并KeepAlive组件到options.components中
  • 初始化相关函数

源码:

    function initGlobalAPI(Vue) {
        // 声明配置对象
        var configDef = {};
        configDef.get = function () {
            // 就是一列的警告,提示等配置项
            return config;
        };
        {
            configDef.set = function () {
                warn(
                    'Do not replace the Vue.config object, set individual fields instead.'
                );
            };
        }
        /**
         * 在这里,为Vue的构造函数,添加一个要通过Object.defineProperty监听的属性config,
         * 获取的时候,获取到的是上面描述的那个config对象,如果对这个config对象直接做变更,
         * 就会提示“不要替换vue.config对象,而是设置单个字段”,说明,
         * vue不希望我们直接去替换和变更整个config对象,如果有需要,希望去直接修改我们需要修改的值
         */
        Object.defineProperty(Vue, 'config', configDef);

        // 暴露的util方法。
        // 注意:这些不是公共API的一部分——避免依赖,除非你意识到其中的风险。

        Vue.util = {
            warn: warn, //警告函数
            extend: extend, //继承方式 
            mergeOptions: mergeOptions, //合并参数
            // 该方法在object上定义一个响应式的属性
            defineReactive: defineReactive 
        };
        Vue.set = set;
        Vue.delete = del; 
        Vue.nextTick = nextTick;

        Vue.options = Object.create(null);
        //添加components ,directives, filters 指令组件 控对象
        ASSET_TYPES.forEach(function (type) {
            Vue.options[type + 's'] = Object.create(null);
        });

        //用来标识扩展所有普通对象的“基”构造函数
        // Weex的多实例场景中的组件。
        Vue.options._base = Vue;

        extend(Vue.options.components, builtInComponents); //合并  KeepAlive参数中的组件对象
        initUse(Vue); //  初始化vue 安装插件函数
        initMixin$1(Vue);  //初始化vue mixin 函数
        initExtend(Vue); //初始化 vue extend 函数
        initAssetRegisters(Vue); //为vue 添加 静态方法component,directive,filter
    }

9-2. initUse

作用:

给Vue挂载一个use插件函数

流程:

  • 如果插件安装过了,则返回当前插件
  • 否则,将插件添加到插件数组开头
  • plugin是个函数则执行安装,并且添加到已安装数组

源码:

    function initUse(Vue) {
        //安装 Vue.js 插件。
        Vue.use = function (plugin) {
            /**
             * 安装的插件放到了 installedPlugins    
             * Vue会去寻找这个插件在已安装的插件列表里有没有,
             * 如果没有,则进行安装插件,如果有则跳出函数,这保证插件只被安装一次。
             */
            var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
            if (installedPlugins.indexOf(plugin) > -1) { //判断是否已经安装过插件了
                return this
            }
            // 转换为真的数组
            var args = toArray(arguments, 1);
            args.unshift(this); //在前面添加
            if (typeof plugin.install === 'function') { //如果plugin.install 是个函数 则执行安装
                plugin.install.apply(plugin, args);
            } else if (typeof plugin === 'function') { //如果plugin 是个函数则安装
                plugin.apply(null, args);
            }
            installedPlugins.push(plugin); // 将已经安装过的插件添加到队列去
            return this
        };
    }

9-3. initMixin$1

作用:

给Vue挂载一个mixin函数,合并mixin参数到Vue的options上,并返回合并对象

源码:

    function initMixin$1(Vue) {
        Vue.mixin = function (mixin) {
            // 合并 对象
            this.options = mergeOptions(this.options, mixin);
            return this
        };
    }

9-4 initExtend

作用:

在Vue上挂载一个extend方法

使用场景:

Vue.extend返回的是一个扩展实例构造器,预设了部分选项的Vue实例构造器。一般用来用来生成组件。比如:以组件名称作为标签的自定义元素时,会自动调用Vue.extend来生产组件实例,并挂载到自定义元素上。

如:

<body>
    // 方式1
    <div id="detailMsg"></div>
    // 方式2
    <detail></detail>
    <script type="text/javascript">
	// 创建构造器
	var Detail = Vue.extend({
			template: '<div>detail message is :{{msg}}</div>',
			data: function () {
				return {
					msg: 'hello world'
				}
			}
	})
	// 创建 Detail 实例,并挂载到对应元素上。 方式1
	new Detail().$mount('#detailMsg')
        
        // 创建 Detail 实例,并挂载到对应标签上。 方式2
	new Detail().$mount('detail')
    </script>
</body>

流程:

  • 通过this._init生成组件对象Sub
  • 给Sub挂载prototype,cid,options,super,extend,mixin,use,superOptions,extendOptions,sealedOptions等属性。
  • 将Sub添加到构造函数的缓存对象里
  • 返回sUB

源码:

    function initExtend(Vue) {
        /**
         * 每个实例构造函数(包括Vue)都有一个唯一的cid。
         * 这使我们能够创建包装的“子对象”用于原型继承和缓存它们的构造函数。
         */
        Vue.cid = 0;
        var cid = 1;

        /**
         * Vue.extend//使用基础 Vue 构造器,创建一个“子类”。
         * 参数是一个包含组件选项的对象。合并继承new 实例化中的拓展参数或者是用户直接使用
         * Vue.extend 的拓展参数。把对象转义成组件构造函数。创建一个sub类 构造函数是VueComponent,
         * 合并options参数,把props属性和计算属性添加到观察者中。
         * 如果组件含有名称 则 把这个对象存到 组件名称中, 在options拓展参数的原型中
         * 能获取到该数据Sub.options.components[name] = Sub 简称Ctor,返回该构造函数
         */

        //使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
        Vue.extend = function (extendOptions) { 
            extendOptions = extendOptions || {};
            var Super = this;
            var SuperId = Super.cid;
            var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); //组件构造函数
            if (cachedCtors[SuperId]) { //父类 超类id
                return cachedCtors[SuperId] //获取 超类
            }
            var name = extendOptions.name || Super.options.name; //获取组件的name
            if ("development" !== 'production' && name) {
                //  验证组件名称 必须是大小写,并且是-横杆
                validateComponentName(name);
            }
            //实例化 组件 对象
            var Sub = function VueComponent(options) {
                // vue中的_init 函数   Vue.prototype._init
                this._init(options);
            };
            //创建一个对象 继承 超类的原型
            Sub.prototype = Object.create(Super.prototype);
            //让他的构造函数指向回来,防止继承扰乱。
            Sub.prototype.constructor = Sub;
            //标识 不同的组件
            Sub.cid = cid++;
            //合并参数
            Sub.options = mergeOptions(
                Super.options,
                extendOptions
            );
            //记录超类
            Sub['super'] = Super;
            //对于道具和计算属性,我们定义代理getter
            //在扩展原型上的扩展时的Vue实例。这避免为创建的每个实例调用Object.defineProperty。
            if (Sub.options.props) { //获取props属性 如果有
                //初始化属性 并且把组件的属性 加入 观察者中
                initProps$1(Sub);
            }
            if (Sub.options.computed) { //组件计算属性
                //定义计算属性 并且 把属性的数据 添加到对象监听中
                initComputed$1(Sub);
            }

            // 允许进一步的扩展/混合/插件使用
            Sub.extend = Super.extend;
            Sub.mixin = Super.mixin;
            Sub.use = Super.use;

            ASSET_TYPES.forEach(function (type) {
                Sub[type] = Super[type];
            });
            //如果组件含有名称 则 把这个对象存到 组件名称中, 在options拓展参数的原型中能获取到该数据
            if (name) { 
                Sub.options.components[name] = Sub;
            }

            //在扩展时保留对超级选项的引用。
            //稍后在实例化时,我们可以检查Super的选项是否具有更新。
            Sub.superOptions = Super.options; //超类 父类的拓展参数
            Sub.extendOptions = extendOptions; //子类拓参数
            Sub.sealedOptions = extend({}, Sub.options); //合并
            // 当前缓存的构造函数
            cachedCtors[SuperId] = Sub;
            return Sub
        };
    }

9-5. initAssetRegisters

作用:

静态方法component,directive,filter

使用场景:


9-6. 结果

vue.prototype没有变更了,vue.options为:

{
    components: {KeepAlive: {…}}, //执行extend(Vue.options.components, builtInComponents),添加的
    directives: {},
    filters: {},
    _base: ƒ Vue(options),
}

10. 监听服务端

// ssr相关
Object.defineProperty(Vue.prototype, '$isServer', {
        get: isServerRendering
});
Object.defineProperty(Vue.prototype, '$ssrContext', {
        get: function get() {
            return this.$vnode && this.$vnode.ssrContext
        }
});
Object.defineProperty(Vue, 'FunctionalRenderContext', {
        value: FunctionalRenderContext
});

Vue.version = '2.5.16'; //版本号

11. 定义判断属性相关函数

// 以下都是通过makeMap函数返回一个函数,去查找map中是否存在val,存在返回true,否则返回false
isReservedAttr // 判断'style,class'
acceptValue    // 判断'input,textarea,option,select,progress'
mustUseProp    // 判断属性和标签是否匹配
isEnumeratedAttr    // 判断'contenteditable(编辑),draggable(拖动),spellcheck(拼写)'
isBooleanAttr  // 检查是否是html中的布尔值属性(属性值只有 true 和 false)
isXlink        // 判断是否是xmlns 属性
getXlinkProp   // 获取xml link的属性
isFalsyAttrValue   // 判断val 是否是 null 或者 false
var namespaceMap = {
     svg: 'http://www.w3.org/2000/svg', //svg标签命名xmlns属性
      math: 'http://www.w3.org/1998/Math/MathML' //math 中的xmlns属性声明 XHTML 文件
};
isFalsyAttrValue   // 判断val 是否是 html中的原始标签
isSVG              // 判断svg 标签 以及 svg子元素标签
isPreTag           // 判断标签是否是pre
isReservedTag      // 判断是不是 html 原生的标签 或者svg标签
getTagNamespace    // 判断 是否是svg 或者 math 标签
unknownElementCache    // 判断 是否是svg 或者 math 标签
isTextInputType    // 判断 是否是文本输入框 判断属性:text,number,password,search,email,tel,url

12. nodeOps冻结节点

12-1. 基本信息

作用:

Object.freeze() 方法可以冻结一个对象,它是作用是:

  • 一个被冻结的对象再也不能被修改;
  • 冻结了一个对象则不能向这个对象添加新的属性
  • 不能删除已有属性
  • 不能修改该对象已有属性的可枚举性、可配置性、可写性
  • 以及不能修改已有属性的值。
  • 此外,冻结一个对象后该对象的原型也不能被修改。
  • freeze() 返回和传入的参数相同的对象。

这里是冻结一系列操作dom的 方法

源码:

var nodeOps = Object.freeze({
        createElement: createElement$1, //创建一个真实的dom
        createElementNS: createElementNS, //创建一个真实的dom svg方式
        createTextNode: createTextNode, // 创建文本节点
        createComment: createComment,  // 创建一个注释节点
        insertBefore: insertBefore,  //插入节点 在xxx  dom 前面插入一个节点
        removeChild: removeChild,   //删除子节点
        appendChild: appendChild,  //添加子节点 尾部
        parentNode: parentNode,  //获取父亲子节点dom
        nextSibling: nextSibling,     //获取下一个兄弟节点
        tagName: tagName,   //获取dom标签名称
        setTextContent: setTextContent, //  //设置dom 文本
        setStyleScope: setStyleScope  //设置组建样式的作用域
    });

12-1 createElement$1

作用:

创建一个真实的dom

流程:

  • 通过标签名创建一个真实的dom
  • 如果不是select标签则返回dom出去
  • 否则,如果是select标签 判断是否设置了multiple属性。如果设置了就重置dom上的multiple属性值为’multiple
  • 最后返回dom

源码:

    function createElement$1(tagName, vnode) {
        // 通过标签名创建一个真实的dom
        var elm = document.createElement(tagName);
        //如果不是select标签则返回dom出去, 退出函数
        if (tagName !== 'select') {
            return elm
        }
        // 否则,如果是select标签 判断是否设置了multiple属性。如果设置了就重置
        if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
            elm.setAttribute('multiple', 'multiple');
        }
        return elm
    }

12-2. createElementNS

作用:

创建一个真实的dom svg方式,document.createElementNS方法可创建带有指定命名空间的元素节点。

用法:

var c=document.createElementNS('http://www.w3.org/2000/svg','svg') //创建svg节点
document.body.appendChild(c);

可以看到这dom中插入了一对svg标签

image.png

源码:

    //创建一个真实的dom svg方式
    function createElementNS(namespace, tagName) {
        /**  namespaceMap,前面定义值为 {
             svg: 'http://www.w3.org/2000/svg',
             math: 'http://www.w3.org/1998/Math/MathML'
            };
            例如:document.createElementNS('http://www.w3.org/2000/svg','svg');
        */ 
        return document.createElementNS(namespaceMap[namespace], tagName)
    }

12-3 createTextNode

作用:

创建文本节点

源码:

function createTextNode(text) {
   return document.createTextNode(text)
}

12-4 createComment

作用:

创建注释节点

用法:

var c=document.createComment("My personal comments"); // 创建注释
document.body.appendChild(c); //插入节点

可以看到这dom中插入了一条注释

image.png

源码:

function createTextNode(text) {
   return document.createTextNode(text)
}

12-5 insertBefore

作用:

在父节点的某个子节点前插入一个新的节点

用法:

node.insertBefore(newnode, existingnode) 方法可在已有的子节点前插入一个新的子节点。

  • node: 被插入的父节点。
  • newnode: 必须。要插入的节点对象
  • existingnode:必须。要添加新的节点前的子节点。

例如:

<ul id="List">
    <li>上海</li>
    <li>深圳</li>
</ul>

在第一个li前面插入一个节点

var newCity = document.createElement("li")  //创建元素
var textnode = document.createTextNode("北京") // 创建元素

newCity.appendChild(textnode)  // 添加文本

var list = document.getElementById("List")

// 在父节点ul的第一个节点li前添加一个newCity节点
list.insertBefore(newCity, list.childNodes[0]);

执行完后:

<ul id="List">
    <li>北京</li>
    <li>上海</li>
    <li>深圳</li>
</ul>

源码:

function insertBefore(parentNode, newNode, referenceNode) {
        parentNode.insertBefore(newNode, referenceNode);
}

12-6 其它节点操作

剩下的比较简单,就放一起讲了

    //删除子节点
    function removeChild(node, child) {
        node.removeChild(child);
    }

    //添加子节点 尾部
    function appendChild(node, child) {
        node.appendChild(child);
    }

    //获取父亲子节点dom
    function parentNode(node) {
        return node.parentNode
    }

    //获取下一个兄弟节点
    function nextSibling(node) {
        return node.nextSibling
    }

    //获取dom标签名称
    function tagName(node) {
        return node.tagName
    }

    //设置dom 文本
    function setTextContent(node, text) {
        node.textContent = text;
    }


    //设置组建样式的作用域,设置后只在当前组件生效
    function setStyleScope(node, scopeId) {
        node.setAttribute(scopeId, '');
    }

到这里,nodeOps冻结节点就讲完了,得到一个包含各种操作dom函数的对象。


13. ref

13-1. 基本信息

作用:

对ref进行一系列的操作,比如对ref 创建,更新 和 销毁

流程:

  • 如果是创建,传入一个vnode,然后将vnode对应的组件或存入到refs上
  • 如果是更新,传入一个oldVnode,删除它上面ref,再传入一个vnode执行创建逻辑
  • 如果是销毁,传入一个vnode,删除它上面ref

源码:

var ref = {
        create: function create(_, vnode) {
            //创建注册一个ref
            registerRef(vnode);
        },
        update: function update(oldVnode, vnode) {
            //更新ref
            if (oldVnode.data.ref !== vnode.data.ref) {

                registerRef(oldVnode, true); //先删除
                registerRef(vnode);  //在添加
            }
        },
        destroy: function destroy(vnode) {
            registerRef(vnode, true); //删除销毁ref
        }
    }

13-2. registerRef

作用:

  • ref:被用来给元素或子组件注册引用信息。引用信息将会注册在父组件$refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

这里,用来注册ref或者删除ref。比如标签上面设置了ref=‘abc’ ,那么该函数就是为this.$refs.abc 注册ref 把真实的dom存进去,最终得到更新后的refs

源码:

    function registerRef(
        vnode, // 虚拟dom对象
        isRemoval // 是否销毁ref, true为销毁,否则为注册
    ) {
        var key = vnode.data.ref;
        if (!isDef(key)) {  // 未定义ref则退出函数
            return
        }
        var vm = vnode.context; // context 上下文
        // ref指向组件的时候,是获取组件实例;指向标签的时候,是获取DOM节点
        // 优先获取vonde的组件实例(对于组件来说),或者el(该Vnode对应的DOM节点,非组件来说)
        var ref = vnode.componentInstance || vnode.elm;
        var refs = vm.$refs;
        // 如果需要销毁ref,从refs上删除对应的ref属性
        if (isRemoval) {
            if (Array.isArray(refs[key])) {
                remove(refs[key], ref);
            } else if (refs[key] === ref) {
                refs[key] = undefined;
            }
        } else {
            if (vnode.data.refInFor) {  //当在v-for之内时,则保存为数组形式
                if (!Array.isArray(refs[key])) { //refs[key] 不是数组 则变成一个数组
                    refs[key] = [ref];
                } else if (refs[key].indexOf(ref) < 0) { //如果ref 不存在 refs的时候则添加进去
                    // $flow-disable-line
                    refs[key].push(ref);
                }
            } else {
                refs[key] = ref; //如果是单个直接赋值
            }
        }
}
    
    
//判断数据是否定义(注意!值为0或者false也是定义过了的)
function isDef(v) {
    return v !== undefined && v !== null
}

14. VNode

作用:

Vnode是一个构造函数,我们用它来生成虚拟节点。挂载了一系列的节点相关的属性和标识。

源码:

//创建一个空的vnode
var emptyNode = new VNode('', {}, []);

// 此时tag为'', data为{},children为[]
var VNode = function VNode(
        tag,  // 当前节点的标签名
        data为{}, // 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型
        children, // 子节点
        text, // 文本
        elm, // 当前节点的dom
        context, // 编译作用域
        componentOptions, // 组件的option选项
        asyncFactory // 异步工厂
        ){
        this.tag = tag;
        this.data = data;
        this.children = children;
        this.text = text;
        this.elm = elm;
        this.ns = undefined; // 当前节点的名字空间
        this.context = context;
        this.fnContext = undefined; // 函数上下文
        this.fnOptions = undefined; // 函数Options选项
        this.fnScopeId = undefined; // 函数范围id
        this.key = data && data.key; // 节点的key属性,被当作节点的标志,用以优化
        this.componentOptions = componentOptions;
        this.componentInstance = undefined; // 当前节点对应的组件的实例
        this.parent = undefined; // 当前节点的父节点

        this.raw = false;
        this.isStatic = false; // 静态节点标志
        this.isRootInsert = true; // 是否作为跟节点插
        this.isComment = false; // 是否为注释节点
        this.isCloned = false; // 是否为克隆节点
        this.isOnce = false; // 是否有v-once指令
        this.asyncFactory = asyncFactory; // 异步工厂
        this.asyncMeta = undefined;
        this.isAsyncPlaceholder = false;
    };

结果:

我们看下生成的emptyNode的结构:

{
    asyncFactory: undefined,
    asyncMeta: undefined,
    isAsyncPlaceholder: false,
    children: [],  // 本次入参
    componentInstance: undefined,
    componentOptions: undefined,
    context: undefined,
    data: {}, // 本次入参
    elm: undefined,
    fnContext: undefined,
    fnOptions: undefined,
    fnScopeId: undefined,
    isCloned: false,
    isComment: false,
    isOnce: false,
    isRootInsert: true,
    isStatic: false,
    key: undefined,
    ns: undefined,
    parent: undefined,
    raw: false,
    tag: "", // 本次入参
    text: undefined,
    child: undefined,
    __proto__: Object
}

15. directives

15-1. 基本信息

作用:

directives和ref对象很相似,都是一个包含create,update,destroy函数属性的对象。

它是用来封装自定义指令用的,vue允许我们去创建自定义指令,例如: v-getval,

那么,指令是用来干嘛的,例如: v-modal, v-for,它们都是更新dom的,所以它是起了一个更新的作用。

源码:

// 这三个属性其实用到的是同一个函数,updateDirectives
var directives = {
        create: updateDirectives, //创建指令
        update: updateDirectives,  //更新指令
        destroy: function unbindDirectives(vnode) {  //销毁指令
            updateDirectives(vnode, emptyNode);
        }
    }

15-2. updateDirectives

作用:

通过_update来更新 新旧vnode,所以我们后面会分析_update

源码:

//更新指令
    function updateDirectives(
        oldVnode, //oldVnode 老数据
        vnode     //vnode 新数据 
        ) {
        // 只要新旧vnode任何一个有指令存在,就更新
        if (oldVnode.data.directives || vnode.data.directives) {
            _update(oldVnode, vnode);
        }
    }

15-3. _update

作用:

更新指令 比较oldVnode和vnode,根据oldVnode和vnode的情况

触发指令钩子函数bind,update,inserted,insert,componentUpdated,unbind钩子函数

流程:

  • 根据新旧节点是否为空来判断操作创建还是销毁

源码:

    function _update(
        oldVnode, //oldVnode 老数据
        vnode     //vnode 新数据 
    ) {
        // 如果旧节点是空节点,就表示当前操作为创建,首次创建
        var isCreate = oldVnode === emptyNode;
        //如果新节点是空节点,就表示当前操作为销毁
        var isDestroy = vnode === emptyNode;
        //规范化的指令,为指令属性修正变成规范的指令数据。返回指令数据集合
        // 这个方法请看下面的解析
        var oldDirs = normalizeDirectives$1(
            oldVnode.data.directives, //vonde指令对象集合
            oldVnode.context //vm vne实例化对象,或者是组件实例化的对象
        );
        //规范化的指令,为指令属性修正变成规范的指令数据。返回指令数据集合
        var newDirs = normalizeDirectives$1(
            vnode.data.directives, //vonde指令对象集合
            vnode.context //vm vne实例化对象,或者是组件实例化的对象
        );

        var dirsWithInsert = []; // 触发inserted指令钩子函数的指令列表。
        var dirsWithPostpatch = []; // 触发componentUpdated钩子函数的指令列表。

        var key, oldDir, dir;
        for (key in newDirs) {      //循环新的指令集合
            oldDir = oldDirs[key];  //获取旧的单个指令值
            dir = newDirs[key];     //获取新的单个指令值

            if (!oldDir) { //新增指令,触发bind
                // new directive, bind 新指令,绑定
                callHook$1(  //例:v-focus指令的bind函数
                    dir, //新的指令值
                    'bind', //触发bind钩子函数
                    vnode,//新的vonde
                    oldVnode //旧的vonde
                );
                if (dir.def && dir.def.inserted) {
                    // 如果有插入指令,加入列表
                    dirsWithInsert.push(dir); 
                }
            } else {
                // 指令已存在触发update
                // existing directive, update 现有的指令,更新
                // 如有指令 <div v-hello='123'></div> value=123. 如果更新了123 就是更新值
                dir.oldValue = oldDir.value; 
                callHook$1(
                    dir,
                    'update',  //触发更新钩子函数
                    vnode,
                    oldVnode
                );
                 // 如果有更新指令,加入列表
                if (dir.def && dir.def.componentUpdated) { 
                    dirsWithPostpatch.push(dir);
                }
            }
        }

        // 此时完成bind和update钩子函数,列表更新完成

        if (dirsWithInsert.length) {
            // 定义一个函数,该函数会执行dirsWithInsert里的每个函数
            var callInsert = function () {
                for (var i = 0; i < dirsWithInsert.length; i++) {
                    callHook$1(
                        dirsWithInsert[i], //新的指令值 也就是上面的dir(新的单个指令值)
                        'inserted', //触发inserted钩子函数
                        vnode, //新的vonde
                        oldVnode //旧的vonde
                    );
                }
            };
            if (isCreate) {
                //如果是初始化  
                mergeVNodeHook(
                    vnode,
                    'insert',//合并钩子函数
                    callInsert
                );
            } else {
                callInsert();
            }
        }

        if (dirsWithPostpatch.length) {
            mergeVNodeHook(vnode,
                'postpatch',
                function () {
                    for (var i = 0; i < dirsWithPostpatch.length; i++) {
                        callHook$1(
                            dirsWithPostpatch[i],
                            'componentUpdated',
                            vnode, oldVnode);
                    }
                });
        }

        if (!isCreate) {
            for (key in oldDirs) {
                if (!newDirs[key]) { //新的vonde 中没有了指令
                    // no longer present, unbind 不再存在,解除束缚
                    callHook$1(
                        oldDirs[key],
                        'unbind', //触发unbind 钩子
                        oldVnode,
                        oldVnode,
                    );
                }
            }
        }
    }

15-4. normalizeDirectives$1

作用:

它修正指令属性变成规范的指令数据,返回指令数据集合

源码:

function normalizeDirectives$1(
        dirs, //vonde 指令集合
        vm //vm vne实例化对象,或者是组件实例化的对象
    ) {
        //创建一个空的对象
        var res = Object.create(null);
        //如果 指令 名称dirs 不存在 则返回一个空的对象
        if (!dirs) {
            // $flow-disable-line
            return res
        }

        var i, dir;
        for (i = 0; i < dirs.length; i++) { //循环遍历指令集合
            dir = dirs[i];
            if (!dir.modifiers) { //判断是否有修饰符
                // $flow-disable-line
                dir.modifiers = emptyModifiers; //空对象
            }
            //返回指令名称 或者属性name名称+修饰符
            res[getRawDirName(dir)] = dir;
            /**
            *给当前指令挂载自定义指令属性,该属性由用户自定义如 
            *bind,inserted,update,componentUpdated,unbind这些
            */
            dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);
        }
        // $flow-disable-line
        return res
    }

15-5. getRawDirName

作用:

返回指令名称 或者属性name名称+修饰符

源码:

    function getRawDirName(dir) {
        //rawName 视图中的 指令如 <div v-hello></div>  就是v-hello
        //name 视图中的 指令如 <div v-hello></div>  就是hello
        //name 视图中的 指令如有修饰符 <div v-hello.native></div>  就是hello.native
        //modifiers 修饰符
        return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.')))
    }

15-6. resolveAsset

此时res[getRawDirName(dir)] = dir,已经将指令名作为res的属性了,并且将指令作为属性值。

接着检测指令是否在 组件对象上面 ,返回注册指令或者组建的对象

    function resolveAsset(
        options, //参数 例:vm.$options
        type, // 类型 例:'directives' , 'filters' ,'components'
        id,   // 指令,组件的key 属性  例:dir.name
        warnMissing //开启警告的信息 例:true
    ) {
        /* istanbul ignore if  如果id不是字符串,退出函数 */
        // 返回逻辑【1】
        if (typeof id !== 'string') {
            return
        }
        var assets = options[type]; // 例: vm.$options['components']
        // check local registration variations first
        /**
         * 首先检查本地注册的变化 判断id(组件等的name)是否是assets自有属性
         * 否则判断将id驼峰后的key,是否是assets自有属性
         * 否则判断将id驼峰后,再首字母变大写的key,是否是assets自有属性
         */
        // 例:判断v-modal 指令,在不在options['directives']中
        // 例:判断my-header 组件,在不在options['components']中
        /**
         * 所以,我们在Vue引入某个组件时候,我们可以在template写组件标签用驼峰的方式,
         * 也可以是首字母大写,或是直接用组件名来当作组件的标签,就是因为这里做了这样的扩展处理
         */

        // 执行返回逻辑【2】
        if (hasOwn(assets, id)) {
            return assets[id]
        }

        //  可以让这样的的属性 v-model 变成 vModel  变成驼峰
        var camelizedId = camelize(id);
        // 执行返回逻辑【3】
        if (hasOwn(assets, camelizedId)) {
            return assets[camelizedId]
        }

        // 将首字母变成大写 即 vModel 变成 VModel
        var PascalCaseId = capitalize(camelizedId);
        // 执行返回逻辑【4】
        if (hasOwn(assets, PascalCaseId)) {
            return assets[PascalCaseId]
        }

        // fallback to prototype chain  回到原型链
        var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
        // 如果以上都不成立且是开发环境则警告
        if ("development" !== 'production' && warnMissing && !res) {
            warn(
                'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
                options
            );
        }
        //返回注册指令或者组建的对象(原型上的)
        return res
    }

接着将我们的指令name变成驼峰的写法,camelize:

/**
     把横线-的转换成驼峰写法
     这个正则可以让这样的的属性 v-model 变成 vModel
     把名称格式为“xx-xx”的变为“xxXx”,这里接收的是当前的props属性值,一个字符串
     */
    var camelizeRE = /-(\w)/g;
    var camelize = cached(function (str) {
        return str.replace(camelizeRE, function (_, c) {
            /**
             * var str="Hello World!"
               str.toUpperCase() //HELLO WORLD! 将小写转为大写
             */
            return c ? c.toUpperCase() : '';
        })
    });

接着看下cached函数:

// 它将我们需要调用的一些函数给封装到一个对象里面,需要的时候就去对象取
function cached(fn) {
        var cache = Object.create(null); //这样创建的没有原型的空对象 
        return (function cachedFn(str) {
            var hit = cache[str];
            return hit || (cache[str] = fn(str))
        })
    }

为了更直接观的查看camelize函数,我们就将cached引入过来,再回到我们前面的camelizeRE函数,是不是清晰了很多:

var camelizeRE = /-(\w)/g;
    var camelize = function () {
        var cache = Object.create(null); //这样创建的没有原型的空对象 
        // 这个str就是某些属性的key,如id,v-modal
        // 也就是说,会先去缓存对象cache中判断有没有存在,id属性对应的函数,有就返回,没有的就设置
        return (function cachedFn(str) {
            var hit = cache[str];
            return hit
            || (cache[str] =  str.replace(camelizeRE, function (_, c) { //后面这一块就是cached的入参fn
                return c ? c.toUpperCase() : '';
            })(str))
        })
        
    };

ok,此时我们已经分析完camelize函数了,现在resolveAsset已经分析完了,拿到了指令集合。normalizeDirectives$1也执行完了,指令属性修正变成规范的指令数据.

接着回到我们的_update函数,新旧的指令集合我们都拿到了:

我们已经分析完了这个函数了,它实现了更新新旧节点指令和执行钩子函数。
回推回去,directives也分析完了,指令章节也分析完了。

16. events

var events = {
    create: updateDOMListeners,
    update: updateDOMListeners
}

16-1. updateDOMListeners

作用:

更新dom事件

源码:

    function updateDOMListeners(oldVnode, vnode) {
        // 边界处理
        if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
            return
        }
        var on = vnode.data.on || {};
        var oldOn = oldVnode.data.on || {};
        target$1 = vnode.elm; //真实的dom
        normalizeEvents(on);    //为事件 多添加 change 或者input 事件加进去
        // 更新数据源 并且为新的值 添加函数 旧的值删除函数等功能
        updateListeners(
            on, //新的事件对象
            oldOn, //旧的事件对象
            add$1, //添加真实dom的事件函数
            remove$2, //删除真实dom的事件函数
            vnode.context //vue 实例化的对象 new Vue 或者组件 构造函数实例化的对象
        );
        target$1 = undefined;
    }

16-2. normalizeEvents

作用:

为事件 多添加 change 或者input 事件加进去

源码:

    function normalizeEvents(on) {
        /* istanbul ignore if */
        if (isDef(on[RANGE_TOKEN])) {
            // IE input[type=range] only supports `change` event
            // 判断是否是ie 浏览器,如果是则选择 change 事件,如果不是则选择input事件
            var event = isIE ? 'change' : 'input';  
            // 连接事件 把change或者input 事件添加进去
            on[event] = [].concat(on[RANGE_TOKEN], on[event] || []); 
            delete on[RANGE_TOKEN]; //删除旧的事件
        }
        // This was originally intended to fix #4521 but no longer necessary
        // after 2.5. Keeping it for backwards compat with generated code from < 2.4
        /* istanbul ignore if */
        //最初的目的是修复#4521,但现在已经没有必要了
        // 2.5之后。保留它以便与< 2.4生成的代码进行反向比较
        //添加change事件
        if (isDef(on[CHECKBOX_RADIO_TOKEN])) {

            on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []);
            delete on[CHECKBOX_RADIO_TOKEN];
        }
    }

16-2. updateListeners

作用:

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

源码:

    function updateListeners(
        on,  //新的事件
        oldOn, //旧的事件
        add,  //添加事件函数
        remove$$1, //删除事件函数
        vm//vue 实例化对象
    ) {
        var name, def, cur, old, event;
        for (name in on) {
            def = cur = on[name];  //on 新的事件值
            old = oldOn[name];  // 旧的值
            event = normalizeEvent(name);   //normalizeEvent 如果是事件,则过滤 事件修饰符

            /* istanbul ignore if */
            // isUndef 值是空的 undefined || null
            if (isUndef(cur)) {
                //如果不是生产环境
                "development" !== 'production' && warn(
                    "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
                    vm
                );
            } else if (isUndef(old)) {

                if (isUndef(cur.fns)) { //如果函数不存在 则绑定函数
                    //函数 获取钩子函数
                    // 创建函数调用器并重新复制给cur和on[name]
                    cur = on[name] = createFnInvoker(cur); //这个时候cur.fns就存在了
                }
                name = '&' + name; // mark the event as passive 将事件标记为被动的
                //添加事件
                add(
                    event.name, //事件名称
                    cur, // 转义过的事件 执行静态类
                    event.once, //是否只触发一次的状态
                    event.capture, //  事件俘获或是冒泡行为
                    event.passive, // 检测事件修饰符 是否是   '&'
                    event.params //事件参数
                );

            } else if (cur !== old) {
                //如果新的值不等于旧的值
                //则更新新旧值
                old.fns = cur;
                on[name] = old;
            }
        }
        for (name in oldOn) {
            //循环旧的值 为空的时候
            if (isUndef(on[name])) {
                //获取事件
                event = normalizeEvent(name);
                //删除旧的值的事件
                remove$$1(event.name, oldOn[name], event.capture);
            }
        }
    }

updateDOMListeners分析完成。

17. updateDOMProps

更新真实dom的props属性

var domProps = {
        create: updateDOMProps, //更新真实dom的props 属性值
        update: updateDOMProps
}

16-2. updateDOMProps

作用:

更新真实dom的props属性

源码:

function updateDOMProps(oldVnode, vnode) {

        if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
            return
        }
        var key, cur;
        var elm = vnode.elm;
        var oldProps = oldVnode.data.domProps || {}; //获取旧的props属性
        var props = vnode.data.domProps || {}; //获取新的props
        // clone observed objects, as the user probably wants to mutate it
        // 克隆观察到的对象,因为用户可能希望对其进行修改
        // 如果是props添加了观察者,重新克隆他,这样就可以修改了
        if (isDef(props.__ob__)) { 
            props = vnode.data.domProps = extend({}, props);
        }

        for (key in oldProps) {
            if (isUndef(props[key])) {
                elm[key] = '';
            }
        }
        for (key in props) {
            cur = props[key]; 
            // ignore children if the node has textContent or innerHTML,
            // as these will throw away existing DOM nodes and cause removal errors
            // on subsequent patches (#3360)
            //忽略子节点,如果节点有textContent或innerHTML,
            //因为这将丢弃现有的DOM节点并导致删除错误
            //其后的修补程式(#3360)
            if (
                key === 'textContent' ||
                key === 'innerHTML'
            ) {
                if (vnode.children) {
                    vnode.children.length = 0;
                }
                if (cur === oldProps[key]) {
                    continue
                }
                // #6601 work around Chrome version <= 55 bug where single textNode
                // replaced by innerHTML/textContent retains its parentNode property
                // #6601解决Chrome版本<= 55的bug,其中只有一个textNode
                //被innerHTML/textContent替换后,保留了它的parentNode属性
                if (elm.childNodes.length === 1) { //文本节点
                    elm.removeChild(elm.childNodes[0]);
                }
            }

            if (key === 'value') {
                // store value as _value as well since
                // non-string values will be stringified
                //将value存储为_value以及since
                //非字符串值将被字符串化
                elm._value = cur;
                // avoid resetting cursor position when value is the same
                // 当值相同时,避免重置光标位置
                var strCur = isUndef(cur) ? '' : String(cur); //转义成字符串
                if (shouldUpdateValue(
                    elm,   //真实的dom
                    strCur //value
                )) {
                    elm.value = strCur; //赋值
                }
            } else {
                elm[key] = cur; //直接赋值
            }
        }
    }

18. style

作用:

style 字符串 转换成对象 比如'width:100px;height:200px;' 转化成 {width:100px,height:200px}

源码:

    var parseStyleText = cached(function (cssText) {
        var res = {};
        //匹配字符串中的 ;符号。但是不属于 (;)的 符号 如果是括号中的;不能匹配出来
        var listDelimiter = /;(?![^(]*\))/g; 
        var propertyDelimiter = /:(.+)/;  //:+任何字符串
        cssText.split(listDelimiter).forEach(function (item) {
            if (item) {
                var tmp = item.split(propertyDelimiter);
                tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim());
            }
        });
        return res
    });

19. platformModules

封装工具模板

    var platformModules = [
        // attrs包含两个方法create和update都是更新设置真实dom属性值 
        // {create: updateAttrs, /*创建属性*/ update: updateAttrs  /*更新属性 */}
        attrs,  
        // klass包含类包含两个方法create和update都是更新calss。
        // 其实就是updateClass方法。 设置真实dom的class
        klass, 
        events, //更新真实dom的事件
        domProps, //更新真实dom的props 属性值
        // 更新真实dom的style属性。有两个方法create 和update 不过函数都是updateStyle更新真实dom的style属性值.
        // 将vonde虚拟dom的css 转义成并且渲染到真实dom的css中
        style, 
        transition // 过度动画
    ]
    var modules = platformModules.concat(baseModules);
    //path 把vonde 渲染成真实的dom
    var patch = createPatchFunction(
        {
            nodeOps: nodeOps,
            modules: modules
        }
    );

20. platformDirectives

封装指令

    // 定义插入更新指令函数
    var directive = {
        inserted: function inserted(el, binding, vnode, oldVnode) {...},
        componentUpdated: function componentUpdated(el, binding, vnode) {...}
    };
    
    var show = {
        bind: function bind(el, ref, vnode) {...},
        update: function update(el, ref, vnode) {...},
        unbind: function unbind(el,
            binding,
            vnode,
            oldVnode,
            isDestroy) {...}
    }
    // 封装指令
    var platformDirectives = {
        model: directive,
        show: show
    }

21. 挂载$mount

    Vue.prototype.$mount = function (el, hydrating) {
        debugger
        // query(el) 获取dom,已经是dom就返回,不是dom并且获取不到,警告提示,创建一个新的dev
        el = el && inBrowser ? query(el) : undefined;
        // 安装组件
        return mountComponent(
            this, // Vue实例
            el,  // 真实dom
            hydrating
        )
    };

   
   
   
   
   
   


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值