Vue源码-从执行过程到创建Watcher

977 阅读6分钟

今天跟大家一起简单学习下,Vue从引入到最终实现页面呈现的过程,中间会涉及到一些比较重要的知识点,比如Watcher等,所以会放在一起便于理解。

1、创建实例

要想使用 Vue 等第三方JS库,都得先从引入开始。一般情况下,单页面应用里,我们会在 main.js 里引入 vue,并创建 vue 实例。

import Vue from 'vue'
new Vue({
    render: h => h(App),
}).$mount('#app')

这两段代码其实都执行了自己的流程。

首先,importvue 文件,会执行文件内定义执行函数去完成一个初始化的功能。其中包括:

initMixin(Vue); // 初始化_init方法,挂载到vue实例上,后面会详细讲解

stateMixin(Vue); // 初始化data,props,watcher等属性

eventsMixin(Vue); // 初始化event事件,v-on等

lifecycleMixin(Vue); // 绑定一些vue事件,比如destory等

renderMixin(Vue); // 绑定_render函数,用以获取vnode

此处需要注意的是,不管引入多次模块,由于 webpack 的优化,里面的执行方法只会运行一次。

其次,new 一个 Vue 实例对象。Vue方法见下方源码。

function Vue (options) {
    if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
    ) {
        warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}

可以清楚的看到,其中只是对生产环境进行了判断,然后执行了 _init 方法,上面讲过是在初始化时,将该方法挂在到了vue方法上,所以直接 this._init()

重点,_init 方法在两个地方会用到,此处是一个,下面会详细讲到。

总结一下大致流程:

  • 首先,importvue 文件执行一次 initMixin 等方法,将 _init 挂到 Vue 方法的原型上,这样就可以直接调用了;
  • 然后在 new Vue() 时,会执行该对象里面的方法,有多少次 new Vue() 就会执行几次(第一个 _init );
  • 一般我们的项目里肯定不止在第一次(main.js)里去创建 Vue 对象,项目里定义每个组件都会作为一个独立的 Vue 组件去创建,在组件注册时,通过 vue.extend() (继承自 vue 的构造器,内部第二个 _init 方法)创建组件实例。主要原理就是通过 Vue.extend 方法把自定义的组件对象传入,然后创建这个组件对应的构造函数。

2、创建Vue组件

先放上关键代码

Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    //。。。。。。
    var Sub = function VueComponent (options) {
        this._init(options); // 此处是第二次使用
    };
   //。。。。。。
    if (Sub.options.props) {
        initProps$1(Sub);
    }
    if (Sub.options.computed) {
        initComputed$1(Sub);
    }
    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.options = mergeOptions( // 父类Vue.options和传入的组件对象进行了合并,这样二者的Sub.options就包含了全局的和局部的属性了。
        Super.options,
        extendOptions
    );
    //。。。。。。
    return Sub
}

举个例子,最外层的 app 组件的 extendOptions 打印结果如下:

image.png

由此可见,这个对象就是 Vue 构造器的子类,所以,此方法也是对传入的属性对象进行 _init 处理成为组件。好的,到这边我们最基本的入口部分的知识才初步有了了解,咱一鼓作气,顺着这个方法继续往下。

这边的 initMixin 方法定义了_init方法:

function initMixin (Vue) {
    Vue.prototype._init = function (options) {
        var vm = this;
        // a uid
        vm._uid = uid$3++; // 每个组件都有个自己的唯一的uid

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

        // 一个阻止它被依赖收集的标志
        vm._isVue = true;
        // merge options
        if (options && options._isComponent) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            initInternalComponent(vm, options);
        } else {
            vm.$options = mergeOptions(
            //将一些vue组件的属性重新处理一下添加到$options上
            //比如normalizeProps(child, vm),处理了props的值
                resolveConstructorOptions(vm.constructor),
                options || {},
                vm
            );
        }
        /* istanbul ignore else */
           //_renderProxy 会在创建VNode时候使用到,它的作用就是控制调用render函数时所指向的上下文,
           //其实就是我们所说的this指向
        if (process.env.NODE_ENV !== 'production') {
            initProxy(vm); // 创建VNode的时候,开发环境下,会设置代理,去处理一些handler事件
        } else {
            vm._renderProxy = vm;
        }
        // expose real self
        vm._self = vm;
        initLifecycle(vm); //处理生命周期
        initEvents(vm); //处理事件监听
        initRender(vm); //创建虚拟节点
        callHook(vm, 'beforeCreate'); //调用beforeCreate生命周期
        initInjections(vm); // resolve injections before data/props
        initState(vm); //初始化data,props,computed,watcher
        initProvide(vm); // resolve provide after data/props
        callHook(vm, 'created'); //调用created生命周期

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

        if (vm.$options.el) {
            vm.$mount(vm.$options.el); // 挂载虚拟节点
        }
    };
}

我们可以看到 _init 函数最后调用了 $mount,该方法就是把模板渲染成被浏览器识别的 DOM,挂载到页面元素上,接下来我们看看 $mount 方法做了什么。

// public mount method
Vue.prototype.$mount = function (
    el,
    hydrating
) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

$mount 方法的第一个参数是绑定挂载节点的,如果是字符串则去 document.querySelector 查找该节点,否则直接返回该节点或 undefined。举例:比如我们的 vue 项目的 html 文件的根节点是 #app,自然将虚拟节点挂载这个上面。

有个注意点, $mount 方法除了这边(初始化 Vue 实例时)用到以外,还会再第一次渲染组件或更新组件内容的时候去重新挂载,下面在遇到 createComponent 方法时会讲到。此处就涉及到 diff 算法,patch等,要学的太多了,不能乱了脑子,回到我们研究的地方。

1丶其中首次调用是在实例化时去调用一下。

new Vue({
  render: h => h(App),
}).$mount('#app')

下面再继续研究下 mountComponent 方法。

function mountComponent (
    vm,
    el,
    hydrating
) {
    vm.$el = el;
    if (!vm.$options.render) { // 没有render函数的情况
        vm.$options.render = createEmptyVNode; // 先创建空的vode
        if (process.env.NODE_ENV !== 'production') {
            /* istanbul ignore if */
            if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
                vm.$options.el || el) {
                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 {
                warn(
                    'Failed to mount component: template or render function not defined.',
                    vm
                );
            }
        }
    }
    callHook(vm, 'beforeMount'); // beforeMount钩子函数

    var updateComponent;
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== '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(); // 调用_render函数
            mark(endTag);
            measure(("vue " + name + " render"), startTag, endTag);

            mark(startTag);
            vm._update(vnode, hydrating); // 调用_update函数
            mark(endTag);
            measure(("vue " + name + " patch"), startTag, endTag);
        };
    } else { // 主要看到这边,updateComponent就是_update方法。
        updateComponent = function () {
            vm._update(vm._render(), hydrating);
        };
    }
//创建watcher实例对象,把刚刚定义的updateComponent方法传入进去
    new Watcher(vm, updateComponent, noop, {
        before: function before () {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate');
            }
        }
    }, true /* isRenderWatcher */);
    hydrating = false;

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, 'mounted');
    }
    return vm
}

在继续理解主线流程前,我们还需要先搞懂一下前面提到的几个函数哪儿来的。

(1) render函数

_render 方法主要做了一件事,调用 render 方法来生成虚拟 DOM 对象。简单讲下方法流程,代码太多会乱,先不考虑这么多。

这边主要通过 vnode = render.call(vm._renderProxy, vm.$createElement);,

调用之前绑在 vue 实例上的 render 方法,主要是在 initRender 方法里。 initRender 主要代码如下:

vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
//。。。。。
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

可以看到主要调用了 createElement,这个方法里主要就是自定义了 _createElement 方法,判断几种情况,生成 VNode。

if (typeof tag === 'string') { // 如果是普通的标签
    var Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    if (config.isReservedTag(tag)) {
        // platform built-in elements

        vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
        );
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
        // 组件形式的情况下
        vnode = createComponent(Ctor, data, context, children, tag);
    } else {
        // 其他情况
        vnode = new VNode(
            tag, data, children,
            undefined, undefined, context
        );
    }
} else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children);
}

createComponent 方法在创建组件(处理 data , props 等)的同时,会去看组件内是否有子组件,有的话,再此去对这些子组件去执行 $mount 操作。

//。。。。。。
var child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
//。。。。。。

由调用该 _render 方法的 $mount 的机制可以看的出来:我们所定义的一个组件,则分别对应一个 VNode对象,同时里面的每个标签一直会循环到最里层,生成一个父子形式的 VNode 结构,代表整个页面的结构与数据。不难看出,当 Vue 机制内的一些方法去运行时,比如比较更新,先对虚拟节点进行处理才将结果输出就可以减少DOM机制的消耗。在拿到我们想要的节点后,紧接着会去执行 _update方法。

(2) _update函数

创建好虚拟DOM后,接下来就是将虚拟DOM转换为真实DOM并挂载。最终挂载真实DOM的逻辑主要就是在这边完成。它在两个地方被调用,一个是首次渲染,另一个是数据更新的时候,这里我们主要还是看我们这边涉及到的第一次渲染。

function lifecycleMixin (Vue) {
    Vue.prototype._update = function (vnode, hydrating) {
        var vm = this;
        var prevEl = vm.$el;
        var prevVnode = vm._vnode; // 第一次触发的话,_vnode是null,prevVnode 也是null
        var restoreActiveInstance = setActiveInstance(vm);
        vm._vnode = vnode; // 给_vnode 赋值,下次在进来时,这个参数就是有值的
        // 此处的作用是,将VNode转换成真实DOM,赋值给$el参数    
        if (!prevVnode) { // 第一次初始化
            // initial render
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
        } else { // 更新时触发
            // updates
            vm.$el = vm.__patch__(prevVnode, vnode);
        }
。。。。。。
        // 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;
        }
    };

这边的主要代码是 patch 方法,定义在Vue.prototype.__patch__ = inBrowser ? patch : noop; ,判断浏览器和非浏览器的情况下,该调用哪种方法。在源码里,是通过createPatchFunction方法来创建这个方法的。_patch__方法的主要逻辑:

  • 如果已存在vnode,那么会调用patchVnode函数来比较之间的区别
  • 如果不存在vnode,实际上会使用挂载点的DOM对象和vnode来进行处理 最后就是经过一些列的比较、更新等操作,创建真实DOM节点并插入的过程。(如果存在子组件的话,会通过递归先创建子组件的内容,再去创建父组件)

那么接下来,我们回到正题。 在实例化 Watcher 对象时,触发了我们上方讲到的一系列函数。其实这边的一个Vue从编译模板到挂载DOM对象的逻辑就跑通了。但我觉得 Watcher 对象的内容帮助我们更加深入理解Vue的机制有很大的帮助,再继续往下深入看一点。

接下来,我们重点看一下 new Watcher 做了什么。

3、Watcher

(注意,此处只更具这一条线,梳理简单的流程,有些知识点这边不会讲到) Watcher 是Vue中的观察者类,主要任务是:观察Vue组件中的属性,当属性更新时作相应的操作,即实例化时传入的回调函数;在Vue实现依赖收集操作的时候,会记录所有的 Watcher,当属性更新时,通知 watcher 执行更新dom操作。

Watcher在Vue里是在三个地方被调用:

  1. 一个是计算属性 computed 创建的 computedWatcher
  2. 一个是监听属性watch创建的 Watcher
  3. 还有一个是用于渲染更新dom的 renderWatcher

一个组件只有一个 renderWatcher,有多个 computedWatcheruserWatcher 。下面针对renderWatcher的情况的事件也都是以 只有一个 renderWatcher 实例对象 来实现的。 我们本文接触到的就是 renderWatcher,它主要就是运行了注册的回调函数 updateComponent

主要代码:

var Watcher = function Watcher (
    vm,
    expOrFn, // renderWatcher的情况下,传入的就是updateComponent函数
    cb,
    options,
    isRenderWatcher
) {
    this.vm = vm;
    if (isRenderWatcher) {
        vm._watcher = this;
    }
    vm._watchers.push(this);
// options
    if (options) {
        this.deep = !!options.deep;
        this.user = !!options.user;
        this.lazy = !!options.lazy;
        this.sync = !!options.sync;
        this.before = options.before;
    } else {
        this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = process.env.NODE_ENV !== 'production'
        ? expOrFn.toString()
        : '';
    // parse expression for getter
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = parsePath(expOrFn);
        if (!this.getter) {
            this.getter = noop;
            process.env.NODE_ENV !== 'production' && warn(
                "Failed watching path: "" + expOrFn + "" " +
                'Watcher only accepts simple dot-delimited paths. ' +
                'For full control, use a function instead.',
                vm
            );
        }
    }
    this.value = this.lazy
        ? undefined
        : this.get();
};

_watcher 就是 isRenderWatcher ,然后所有的 Watcher 对象会存进 _watchers 数组里。

此处代码的最后,通过调用Watcer原型的get()方法,触发updateComponent方法的同时,将该Watcer实例赋值给Dep.target,给后来使用。

Watcher.prototype.get = function get () {
  pushTarget(this); // Dep.target 赋值
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm); // 触发getter(updateComponent)方法
  } catch (e) {
    if (this.user) { // 自己创建的watcher
      handleError(e, vm, ("getter for watcher "" + (this.expression) + """));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget(); // 清除 Dep.target
    this.cleanupDeps(); // 清理并将newDeps赋值给Wathcer内部的deps,确保内部deps里只存在本次新加入的Watchers
  }
  return value
};
  1. 当我们创建 Watcher 时,便会触发get方法;
  2. 给 Dep.target = 当前的Watcher,在触发 depend 方法时,存入 Dep 对象的 subs 数组,用来完成通知DOM元素更新的操作,同时也会存进 Watcher的newDeps 数组里;
  3. 当是 isRenderWatcher 的情况下,getter是函数 updateComponent
  4. cleanupDeps,清理并将newDeps赋值给Wathcer内部的deps;

我们来看一下depend 方法:


Dep.prototype.depend = function depend () { // Dep.target是唯一值,用来定义当前在处理
  if (Dep.target) {
    Dep.target.addDep(this); // 此处的this是Dep
  }
};

/**
 * Add a dependency to this directive.
 */
Watcher.prototype.addDep = function addDep (dep) {
//dep就是创建的Dep对象,在获取get事件值的触发的
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
        this.newDepIds.add(id); // 加入newDepIds
        this.newDeps.push(dep); // 加入newDeps,后面会存入deps,然后清空newDeps
        if (!this.depIds.has(id)) { // 对subs内的watcher去重
            dep.addSub(this); // depIds不存在的话,则加入subs
        }
    }
};

可以看到,其实 DepWatcher 是相互依赖的。当将 Watcer 存入 Dep 的 subs 数组的同时,Watcer 也会将该 Dep 存入本身的 deps 属性里面。

然后,当被监听的data发生改变时,会触发set方法,从而调用 dep.notify(),会通知对应 subs 里的 Watcher 调用对应的 update 方法;同时,根据上面说到的Watcer 本身的 deps 属性里面的Dep对象,它是干嘛的呢?根据所得的依赖关系,通过遍历计算属性里面的watcher对象的deps,再重新去订阅计算属性的变化。

Watcher.prototype.depend = function depend () {
  var i = this.deps.length;
  while (i--) {
    this.deps[i].depend();
  }
};

notify函数:

Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (process.env.NODE_ENV !== 'production' && !config.async) {
        // subs aren't sorted in scheduler if not running async
        // we need to sort them now to make sure they fire in correct
        // order
        subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

举个例子:

<template>
<div>
{{msg}}
</div>
</template>

<script>
export default {
    name: 'HelloWorld',
    data(){
        return {
            msg: 'data'
        }
    },
    watch:{
        msg(newVal){
            console.log(newVal)
        }
    },
}
</script>

Vue实例里的 _watchers 里有两个元素,一个时 renderWatcher 和自定义的 watcher

1、自定义的 watcher

image.png

这张图对应的是自己创建的 watcher,其中的user属性为true。可以看到 deps: [Dep] , 从前面的总结不难看出,这里的Dep就是指的是,建立 msg(观察属性)时所依赖的data中的 msg 属性所对应 dep。

2、renderWatcher

image.png

这张图是创建的 renderWatcher,从 expression、getter属性可以看的出来。 可以看到其中的 deps 是有值的,有一个值,此值就是在编辑模板上 {{msg}} 时,所创建的dep,此时存在依赖关系。当data数据里的msg发生改变,触发 set 的同时,以此 dep 为媒介,通知它的 Watcher 去执行DOM更新操作。

4、最后

到这边,一个简单的关于 Vue 实例化并挂载到页面,到最后创建 Watcher 实现监听的流程就结束了。这边只是针对其中的一个小方向进行了学习和记录,所以会有很多没讲到的其他知识点。跟我一样刚开始学习Vue源码的有机会可以一起学习。