Skip to content

源码笔记(六):mount 阶段生成 vnode

接上文,在触发生命周期钩子 beforeMount 后,执行:

实例化 渲染 watcher

然后根据 config.performancemark 是否存在,得到不同的 updateComponent,此处为:

js
updateComponent = function () {
  vm._update(vm._render(), hydrating);
};

然后实例化 渲染 watcher

js
new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before: function before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    },
  },
  true
);

渲染 watcher 会触发 render 渲染 vnode,在渲染过程中,get 过程中,涉及到的所有变量都会添加此 watcher 作为订阅者。也就意味着在任一变量发生变化都会通知此 watcher 执行 updateComponent 方法。

前面已知,在实例化 Watcher 的过程中,会执行 this.get -> this.getter 去获取当前 value。此时执行的 this.getter 即为 updateComponent。所以得知实例化 渲染 watcher 分两步:

  1. 执行 vm._renderrender 转化为 vnode,在 render 的过程中,读取到的所有变量都会触发对应的 get 将本 渲染 watcher 加入订阅,也就意味着在任一变量发生变化都会通知此 渲染watcher 执行 updateComponent
  2. 执行 vm._update 将 得到的新 vnode 与旧 vnode 比较,最小差异的更新真实 dom

执行 render 生成 vnode

先执行 vm._render,内部执行:

js
if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots);
}

如果是子组件实例,即 _parentVnode 为父组件 vnode,并将其赋给 vm.$node。然后通过 normalizeScopedSlots 处理了作用域插槽相关。然后执行:

js
vnode = render.call(vm._renderProxy, vm.$createElement); //vm._renderProxy 在 initProxy 定义,vm.$createElement 在 initRender 定义

render 为渲染函数,此方法渲染生成返回一个 virtual dom

Virtual DOM

Virtual DOM 建立在 DOM 之上,是基于 DOM 的一层抽象,实际可理解为用更轻量的纯 JavaScript 对象(树)描述 DOM(树),通过对比 Virtual DOM,只更新需要更新的 DOM 节点。

通常情况下,找到两棵任意的树之间最小修改的时间复杂度是 O(n^3)Virtual DOM 根据前端实际场景,以深度优先,只进行同级比较,复杂度为 O(n)

snabbdom 就是 Virtual DOM 的一个简洁实现。

分析 render 函数

demo 编译出的 render 函数为:

js
(function anonymous() {
  with (this) {
    return _c(
      'div',
      { attrs: { id: 'main' } },
      [
        _c('bpp'), // <Bpp></Bpp>
        _v(' '),
        _c('div', { on: { click: plus } }, [_v('info.name:' + _s(info.name) + ',计算属性:' + _s(compute))]), //<div v-on:click="plus">info.name:{{info.name}},计算属性:{{compute}}</div>
        _v(' '),
        _c('app', { attrs: { name: 'one', num: info.age } }), //<App name="one" v-bind:num="info.age"></App>
        _v(' '),
        _c('div', { on: { click: hide } }, [_v('====点击让第二个App组件卸载====')]), // <div v-on:click="hide">====点击让第二个App组件卸载====</div>
        _v(' '),
        isShow ? _c('app', { attrs: { name: 'two' } }) : _e(), // <App name="two" v-if="isShow"></App>
      ],
      1
    );
  }
});

以上共 9 个子节点,具体的执行 render 中过程不具体分析,只说明其中的一些要点:

  • _c 返回一个普通 vnode_v 返回一个文本 vnode_e 返回一个注释 vnode_s 返回一个字符串, _l 返回一个 vnode 数组 , _u 返回 scopedSlotskeyfn 的键值对,_t 返回 scopedSlot 渲染的插槽 vnode
  • 其中读取每一个变量及 _c,_v,_s,_l 等挂载在 vm 下面的方法都会触发 hasHandler 检查。
  • 读取到 infodata 内的属性时触发监听会把这个 watcher 加到各自的 dep 订阅列表里面,并获得最新值。
  • _stoString 执行 JSON.stringify 得到字符串的过程中,如果变量是对象则会触发该变量及其变量里的每一个属性的 reactiveGetter,即将 渲染 watcher 加到各属性的订阅列表。
  • 读取到 compute 等计算属性触发监听走的 get 方法为 computedGetter,里面取得他自己之前的 watcher,然后 evaluate 惰性求值执行 compute 函数,执行过程中读取了 info.age,所以将他的 watcher 订阅到 info.age 的订阅列表里,同时也取得了最新的 compute 的值。所以在 info.age 变化时,就会通知该 计算 wather 触发更新即设置标识位 dirtytrue,继而在通知 渲染 watcher 触发更新时获取 compute 取值时重新计算。
  • 静态节点的构建会调用 _mrenderStatic 方法,根据传入的索引去执行对应的 render 得到 vnode,并增加属性 isStatic,key,isOnce
  • 执行到数组渲染方法 _lrenderList,在方法内部循环对数组执行对应的 render 方法(_l 的第二个方法参数),最终返回 [VNode, VNode, VNode, _isVList: true],其中每一项 vnode 下有 key 值和 vnode.data 里多了一个 key 属性。数组会在最后的 _c 方法里 normalizeChildren 拍平。
  • 读取到 <App>,<Bpp> 等同步异步组件,组件生成 vnode 下面单独说明。

render 同步组件生成 vnode

执行 _c('app')->createElement->_createElement,在 _createElement 里,因为组件名不为保留标签(config.isReservedTag(tag)),所以执行:

js
//...
else if ((!data || !data.pre) && isDef((Ctor = resolveAsset(context.$options, 'components', tag)))) {
// component
vnode = createComponent(Ctor, data, context, children, tag);
}
//...

其中 children 为 插槽 Vnode

执行 resolveAsset 方法获取该组件在 $options.components 里对应的的组件上下文对象对应的经过 webpack 编译后包含 render 的组件选项对象,赋给 Ctor

构造子类构造函数

然后执行 createComponent 方法,内部执行:

js
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor);
}

baseCtor 即为 Vue 构造函数,extend 即为 Vue.extend。使用基础 Vue 构造器,创建一个“子类”。参数是组件选项对象。

extend 里先读取缓存 Ctor 下的 _Ctor,如果没有,将在构造构造函数结束后将 Ctor 即构造函数存入缓存。 这样在引入多个相同组件的时候,不用重复构造组件的构造函数了。

extend 里通过 validateComponentName 验证组件名之后,继续执行:

js
var Sub = function VueComponent(options) {
  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;

定义了子类构造函数 Sub,并在 Sub 上设置了相关属性,建立了父组件和本组件之类的继承关系。

如果组件的 options 里有 propscomputed,则添加监听挂载到 Sub 的原型即父组件的原型上。 最终返回 Sub 赋给 CtorVue.extend 执行结束。Ctor 即为 Vue component 子组件构造函数。

处理属性及安装组件钩子函数

然后依次判断是否是异步组件 -> 处理 options(通过 resolveConstructorOptions)-> 提取 props(通过 extractPropsFromVNodeData)-> 判断是否是函数组件 -> 提取 listeners 事件 -> 判断是否是 keepAlive/transition 组件,然后执行:

js
installComponentHooks(data);

安装合并 data(属性)里的组件钩子函数: hooks:init,prepatch,insert,destroy

实例化 vnode

然后一切准备工作结束后,调用 new VNode 方法生成组件 vnode(其中前面生成的 Ctor 挂载在 vnode.componentOptions 上,并且组件的 vnode 是没有 children 的,插槽 children 保存在了 componentOptions 上 )。

js
{
  tag: "vue-component-1-app"
  data: {attrs: {…}, on: undefined, hook: {…}}
  children: undefined
  text: undefined
  elm: undefined
  ns: undefined
  context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
  fnContext: undefined
  fnOptions: undefined
  fnScopeId: undefined
  key: undefined
  componentOptions: {propsData: {…}, listeners: undefined, tag: "app", children: undefined, Ctor: ƒ}
  componentInstance: undefined
  parent: undefined
  raw: false
  isStatic: false
  isRootInsert: true
  isComment: false
  isCloned: false
  isOnce: false
  asyncFactory: undefined
  asyncMeta: undefined
  isAsyncPlaceholder: false
}

最终通过 vm._render() 得到整个 vnode,到此,通过 render 构建 vnode 过程结束。

render 异步组件生成 vnode

第一阶段

同同步组件一致,得到 Ctor 为经 webpack 编译后的 Bpp 函数(而同步组件是一个组件选项对象):

js
() => __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./bpp.vue */ './src/bpp.vue'));

然后进入 createComponent,跳过构造子类构造函数,执行:

js
if (isUndef(Ctor.cid)) {
  asyncFactory = Ctor;
  Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
  //...
}

resolveAsyncComponent

resolveAsyncComponent 里,如果提供的异步组件选项是对象的形式,则先处理 error 等各配置。然后将 currentRenderingInstance(即 vue 实例) 赋到 Bpp 函数的 owners 属性上并定义 forceRender,resolve,reject 等异步函数钩子,然后执行:

js
var res = factory(resolve, reject);

res 即为一个 promise,该 promise 会在 __webpack_require__.bind(null, /*! ./bpp.vue */ './src/bpp.vue') 执行完成后的回调里执行。然后对结果 res 做了一些判断处理,本 demo 执行:

js
res.then(resolve, reject);

意味着当 bpp.vue 加载完成后,就会来执行之前定义的 resolve,reject 回调。然后返回空,resolveAsyncComponent 执行结束。

回到 createComponent,将 resolveAsyncComponent 结果赋给 Ctor,因为为空,则返回:

js
return createAsyncPlaceholder(asyncFactory, data, context, children, tag);

调用 createEmptyVNode 返回一个占位的空 vnode(注释类型):

js
{
  tag: undefined
  data: undefined
  children: undefined
  text: ""
  elm: undefined
  ns: undefined
  context: undefined
  fnContext: undefined
  fnOptions: undefined
  fnScopeId: undefined
  key: undefined
  componentOptions: undefined
  componentInstance: undefined
  parent: undefined
  raw: false
  isStatic: false
  isRootInsert: true
  isComment: true
  isCloned: false
  isOnce: false
  asyncFactory: () => {…}
  asyncMeta: {data: undefined, context: Vue, children: undefined, tag: "bpp"}
  isAsyncPlaceholder: false
}

异步组件的 vnode 创建第一阶段结束。

第二阶段

引入异步 bpp.vue 后,执行 resolve 回调:

js
factory.resolved = ensureCtor(res, baseCtor);

其中 resmodule.exportsbaseCtorVue 构造函数。ensureCtor 里先取得 module.exports.default,然后同同步组件一致,执行构造子类构造函数:

js
return isObject(comp) ? base.extend(comp) : comp;

将构造后的子类构造函数 Vue.component 赋给 factory.resolved,执行 forceRender

js
for (var i = 0, l = owners.length; i < l; i++) {
  owners[i].$forceUpdate();
}

owners[]vue 实例,对每一个拥有该组件的父组件执行 $forceUpdate 强制更新。

$forceUpdate

迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件(不影响作用域插槽),而不是所有子组件。

$forceUpdate 方法里执行:vm._watcher.update() 进入渲染 watcher 更新流程。在触发父组件 vue 钩子 beforeUpdate 后,执行 vm._update(vm._render(), hydrating)(中间流程在本系列后续篇章详解)

此时,再次调用 vm._render,其他节点渲染成 vnode 不变,而对于该异步节点渲染,方法里再次进入 resolveAsyncComponent

js
if (isDef(factory.resolved)) {
  return factory.resolved;
}

与第一次不一样的是,本次 factory.resolved 有值为子类构造函数 Vue.component,所以直接返回,就不走之前resolveAsyncComponent 方法里剩下的逻辑了,然后在 createComponent 里就跟同步组件路线一致,生成 vnode

然后会执行 vm._update 方法更新真实 dom,异步 vnode 会通过 createElm 创建一个新的组件对应的真实 dom,所以会依次触发 async Bpp beforeCreate->async Bpp created->async Bpp beforeMount->async Bpp mounted,其中 async Bpp mounted 钩子在父组件的 patchinvokeInsertHook 中触发。

另外,异步组件的强制更新会引起父组件里的其他子组件执行 updateChildComponent,如果该子组件判断有普通插槽或动态插槽(不包含具名插槽),则会强行渲染包含插槽的子组件:

js
if (needsForceUpdate) {
  vm.$slots = resolveSlots(renderChildren, parentVnode.context);
  vm.$forceUpdate();
}

然后在 flushSchedulerQueue 里执行 callUpdatedHooks(updatedQueue) 触发父组件 vue updated 钩子,异步组件加载完成。

本章小结

  1. 本章介绍了 vue 执行的 Mount 阶段中的通过 render 生成 vnode 部分。
  2. 在执行实例化 渲染 watcher 时,触发 render 生成 vnode
  3. 分析了普通节点 render、同步/异步组件 render 的过程。