跳至主要內容

new Vue 发生了什么

h7mlvuevue大约 4 分钟约 1204 字

new Vue 发生了什么

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

初始化 vue

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

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);
}

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

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this;

  // 省略一系列其它初始化的代码

  if (vm.$options.el) {
    console.log('vm.$options.el:', vm.$options.el);
    vm.$mount(vm.$options.el);
  }
};

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

Vue 实例挂载

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

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  el = el && query(el);

  // 省略一系列初始化以及逻辑判断代码

  return mount.call(this, el, hydrating);
};

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

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

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

export function mountComponent(vm: Component, el: ?Element, hydrating?: boolean): Component {
  vm.$el = el;
  // 省略一系列其它代码
  let updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 生成虚拟 vnode
      const vnode = vm._render();
      // 更新 DOM
      vm._update(vnode, hydrating);
    };
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating);
    };
  }

  // 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      },
    },
    true /* isRenderWatcher */
  );
  hydrating = false;

  return vm;
}

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

创建虚拟 Node

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

 Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    let vnode
    try {
      // 省略一系列代码
      currentRenderingInstance = vm
      // 调用 createElement 方法来返回 vnode
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`){}
    }
    // set parent
    vnode.parent = _parentVnode
    console.log("vnode...:",vnode);
    return vnode
  }

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

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

export function _createElement(
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 省略一系列非主线代码

  if (normalizationType === ALWAYS_NORMALIZE) {
    // 场景是 render 函数不是编译生成的
    children = normalizeChildren(children);
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 场景是 render 函数是编译生成的
    children = simpleNormalizeChildren(children);
  }
  let vnode, ns;
  if (typeof tag === 'string') {
    let Ctor;
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
    if (config.isReservedTag(tag)) {
      // 创建虚拟 vnode
      vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context);
    } else if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      vnode = new VNode(tag, data, children, undefined, undefined, context);
    }
  } else {
    vnode = createComponent(tag, data, context, children);
  }
  if (Array.isArray(vnode)) {
    return vnode;
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns);
    if (isDef(data)) registerDeepBindings(data);
    return vnode;
  } else {
    return createEmptyVNode();
  }
}

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

更新视图

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

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this;
  const prevEl = vm.$el;
  const prevVnode = vm._vnode;
  const restoreActiveInstance = setActiveInstance(vm);
  vm._vnode = vnode;
  if (!prevVnode) {
    // 第一个参数为真实的node节点,则为初始化
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
  } else {
    // 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
    vm.$el = vm.__patch__(prevVnode, vnode);
  }
  restoreActiveInstance();
  // update __vue__ reference
  if (prevEl) {
    prevEl.__vue__ = null;
  }
  if (vm.$el) {
    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;
  }
};

总结

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

img
img