new Vue 发生了什么

写在前面:本文基于vue2.6进行源码分析

初始化 vue

我们在实例化一个 vue 实例,也即 new Vue() 时,实际上是执行 src/core/instance/index.js 中定义的 Function 函数。

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

通过查看 Vuefunction,我们知道 Vue 只能通过 new 关键字初始化,然后调用 this._init 方法,该方法在 src/core/instance/init.js 中定义。

1Vue.prototype._init = function (options?: Object) {
2  const vm: Component = this;
3
4  // 省略一系列其它初始化的代码
5
6  if (vm.$options.el) {
7    console.log('vm.$options.el:', vm.$options.el);
8    vm.$mount(vm.$options.el);
9  }
10};

Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

Vue 实例挂载

Vue 中是通过 $mount 实例方法去挂载 dom 的,下面我们通过分析 compiler 版本的 mount 实现,相关源码在目录 src/platforms/web/entry-runtime-with-compiler.js 文件中定义:。

1const mount = Vue.prototype.$mount;
2Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
3  el = el && query(el);
4
5  // 省略一系列初始化以及逻辑判断代码
6
7  return mount.call(this, el, hydrating);
8};

我们发现最终还是调用用原先原型上的 $mount 方法挂载 ,原先原型上的 $mount 方法在 src/platforms/web/runtime/index.js 中定义 。

1Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
2  el = el && inBrowser ? query(el) : undefined;
3  return mountComponent(this, el, hydrating);
4};

我们发现$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中

1export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
2  vm.$el = el;
3  // 省略一系列其它代码
4  let updateComponent;
5  /* istanbul ignore if */
6  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
7    updateComponent = () => {
8      // 生成虚拟 vnode
9      const vnode = vm._render();
10      // 更新 DOM
11      vm._update(vnode, hydrating);
12    };
13  } else {
14    updateComponent = () => {
15      vm._update(vm._render(), hydrating);
16    };
17  }
18
19  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
20  new Watcher(
21    vm,
22    updateComponent,
23    noop,
24    {
25      before() {
26        if (vm._isMounted && !vm._isDestroyed) {
27          callHook(vm, 'beforeUpdate');
28        }
29      },
30    },
31    true /* isRenderWatcher */
32  );
33  hydrating = false;
34
35  return vm;
36}

从上面的代码可以看到,mountComponent核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent方法,在此方法中调用 vm._render方法先生成虚拟 Node,最终调用 vm._update更新 DOM。

创建虚拟 Node

Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟Node。它的定义在 src/core/instance/render.js 文件中:

1Vue.prototype._render = function (): VNode {
2    const vm: Component = this
3    const { render, _parentVnode } = vm.$options
4    let vnode
5    try {
6      // 省略一系列代码
7      currentRenderingInstance = vm
8      // 调用 createElement 方法来返回 vnode
9      vnode = render.call(vm._renderProxy, vm.$createElement)
10    } catch (e) {
11      handleError(e, vm, `render`){}
12    }
13    // set parent
14    vnode.parent = _parentVnode
15    console.log("vnode...:",vnode);
16    return vnode
17  }

vm._render 最终是通过执行 createElement 方法并返回的是 vnode,它是一个虚拟 Node

Vue.js 利用 _createElement 方法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:

1export function _createElement(
2  context: Component,
3  tag?: string | Class<Component> | Function | Object,
4  data?: VNodeData,
5  children?: any,
6  normalizationType?: number
7): VNode | Array<VNode> {
8  // 省略一系列非主线代码
9
10  if (normalizationType === ALWAYS_NORMALIZE) {
11    // 场景是 render 函数不是编译生成的
12    children = normalizeChildren(children);
13  } else if (normalizationType === SIMPLE_NORMALIZE) {
14    // 场景是 render 函数是编译生成的
15    children = simpleNormalizeChildren(children);
16  }
17  let vnode, ns;
18  if (typeof tag === 'string') {
19    let Ctor;
20    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
21    if (config.isReservedTag(tag)) {
22      // 创建虚拟 vnode
23      vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context);
24    } else if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
25      // component
26      vnode = createComponent(Ctor, data, context, children, tag);
27    } else {
28      vnode = new VNode(tag, data, children, undefined, undefined, context);
29    }
30  } else {
31    vnode = createComponent(tag, data, context, children);
32  }
33  if (Array.isArray(vnode)) {
34    return vnode;
35  } else if (isDef(vnode)) {
36    if (isDef(ns)) applyNS(vnode, ns);
37    if (isDef(data)) registerDeepBindings(data);
38    return vnode;
39  } else {
40    return createEmptyVNode();
41  }
42}

_createElement 方法有 5 个参数,context表示 VNode 的上下文环境,它是 Component类型;tag表示标签,它可以是一个字符串,也可以是一个 Componentdata表示 VNode 的数据,它是一个 VNodeData类型,可以在 flow/vnode.js 中找到它的定义;children表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode 数组;

更新视图

完成视图的更新工作事实上就是调用了vm._update方法,这个方法接收的第一个参数是刚生成的Vnode,调用的vm._update方法定义在 src/core/instance/lifecycle.js 中。

1Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
2  const vm: Component = this;
3  const prevEl = vm.$el;
4  const prevVnode = vm._vnode;
5  const restoreActiveInstance = setActiveInstance(vm);
6  vm._vnode = vnode;
7  if (!prevVnode) {
8    // 第一个参数为真实的node节点,则为初始化
9    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
10  } else {
11    // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
12    vm.$el = vm.__patch__(prevVnode, vnode);
13  }
14  restoreActiveInstance();
15  // update __vue__ reference
16  if (prevEl) {
17    prevEl.__vue__ = null;
18  }
19  if (vm.$el) {
20    vm.$el.__vue__ = vm;
21  }
22  // if parent is an HOC, update its $el as well
23  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
24    vm.$parent.$el = vm.$el;
25  }
26};

总结

下图更直观地看到从初始化 Vue 到最终渲染的整个过程。

img