Vue2.x(含组件)主流程源码笔记(六):mount 阶段之生成 vnode

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

实例化 渲染 watcher

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

1
2
3
updateComponent = function () {
vm._update(vm._render(), hydrating);
};

然后实例化 渲染 watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
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,内部执行:

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

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

1
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 函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(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 触发更新即设置标识位 dirty,在后续 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)),所以执行:

1
2
3
4
5
6
//...
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 方法,内部执行:

1
2
3
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}

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

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

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

1
2
3
4
5
6
7
8
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 组件,然后执行:

1
installComponentHooks(data);

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

实例化 vnode

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
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 函数(而同步组件是一个组件选项对象):

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

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

1
2
3
4
5
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
//...
}

resolveAsyncComponent

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

1
var res = factory(resolve, reject);

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

1
res.then(resolve, reject);

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
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 回调:

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

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

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

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

1
2
3
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

1
2
3
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,如果该子组件判断有普通插槽或动态插槽(不包含具名插槽),则会强行渲染包含插槽的子组件:

1
2
3
4
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 的过程。
---- 本文结束,感谢您的阅读 ----