源码笔记(六):mount 阶段生成 vnode
接上文,在触发生命周期钩子 beforeMount 后,执行:
实例化 渲染 watcher
然后根据 config.performance 及 mark 是否存在,得到不同的 updateComponent,此处为:
updateComponent = function () {
vm._update(vm._render(), hydrating);
};然后实例化 渲染 watcher:
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 分两步:
- 执行
vm._render将render转化为vnode,在render的过程中,读取到的所有变量都会触发对应的get将本渲染 watcher加入订阅,也就意味着在任一变量发生变化都会通知此渲染watcher执行updateComponent; - 执行
vm._update将 得到的新vnode与旧vnode比较,最小差异的更新真实dom。
执行 render 生成 vnode
先执行 vm._render,内部执行:
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots);
}如果是子组件实例,即 _parentVnode 为父组件 vnode,并将其赋给 vm.$node。然后通过 normalizeScopedSlots 处理了作用域插槽相关。然后执行:
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 函数为:
(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返回scopedSlots的key和fn的键值对,_t返回scopedSlot渲染的插槽vnode。- 其中读取每一个变量及
_c,_v,_s,_l等挂载在vm下面的方法都会触发hasHandler检查。 - 读取到
info等data内的属性时触发监听会把这个watcher加到各自的dep订阅列表里面,并获得最新值。 _s即toString执行JSON.stringify得到字符串的过程中,如果变量是对象则会触发该变量及其变量里的每一个属性的reactiveGetter,即将渲染 watcher加到各属性的订阅列表。- 读取到
compute等计算属性触发监听走的get方法为computedGetter,里面取得他自己之前的watcher,然后evaluate惰性求值执行compute函数,执行过程中读取了info.age,所以将他的watcher订阅到info.age的订阅列表里,同时也取得了最新的compute的值。所以在info.age变化时,就会通知该计算 wather触发更新即设置标识位dirty为true,继而在通知渲染 watcher触发更新时获取compute取值时重新计算。 - 静态节点的构建会调用
_m即renderStatic方法,根据传入的索引去执行对应的render得到vnode,并增加属性isStatic,key,isOnce。 - 执行到数组渲染方法
_l即renderList,在方法内部循环对数组执行对应的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)),所以执行:
//...
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 方法,内部执行:
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}baseCtor 即为 Vue 构造函数,extend 即为 Vue.extend。使用基础 Vue 构造器,创建一个“子类”。参数是组件选项对象。
extend 里先读取缓存 Ctor 下的 _Ctor,如果没有,将在构造构造函数结束后将 Ctor 即构造函数存入缓存。 这样在引入多个相同组件的时候,不用重复构造组件的构造函数了。
extend 里通过 validateComponentName 验证组件名之后,继续执行:
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 里有 props 及 computed,则添加监听挂载到 Sub 的原型即父组件的原型上。 最终返回 Sub 赋给 Ctor,Vue.extend 执行结束。Ctor 即为 Vue component 子组件构造函数。
处理属性及安装组件钩子函数
然后依次判断是否是异步组件 -> 处理 options(通过 resolveConstructorOptions)-> 提取 props(通过 extractPropsFromVNodeData)-> 判断是否是函数组件 -> 提取 listeners 事件 -> 判断是否是 keepAlive/transition 组件,然后执行:
installComponentHooks(data);安装合并 data(属性)里的组件钩子函数: hooks:init,prepatch,insert,destroy。
实例化 vnode
然后一切准备工作结束后,调用 new VNode 方法生成组件 vnode(其中前面生成的 Ctor 挂载在 vnode.componentOptions 上,并且组件的 vnode 是没有 children 的,插槽 children 保存在了 componentOptions 上 )。
{
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 函数(而同步组件是一个组件选项对象):
() => __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./bpp.vue */ './src/bpp.vue'));然后进入 createComponent,跳过构造子类构造函数,执行:
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor;
Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
//...
}resolveAsyncComponent
resolveAsyncComponent 里,如果提供的异步组件选项是对象的形式,则先处理 error 等各配置。然后将 currentRenderingInstance(即 vue 实例) 赋到 Bpp 函数的 owners 属性上并定义 forceRender,resolve,reject 等异步函数钩子,然后执行:
var res = factory(resolve, reject);res 即为一个 promise,该 promise 会在 __webpack_require__.bind(null, /*! ./bpp.vue */ './src/bpp.vue') 执行完成后的回调里执行。然后对结果 res 做了一些判断处理,本 demo 执行:
res.then(resolve, reject);意味着当 bpp.vue 加载完成后,就会来执行之前定义的 resolve,reject 回调。然后返回空,resolveAsyncComponent 执行结束。
回到 createComponent,将 resolveAsyncComponent 结果赋给 Ctor,因为为空,则返回:
return createAsyncPlaceholder(asyncFactory, data, context, children, tag);调用 createEmptyVNode 返回一个占位的空 vnode(注释类型):
{
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 回调:
factory.resolved = ensureCtor(res, baseCtor);其中 res 为 module.exports,baseCtor 为 Vue 构造函数。ensureCtor 里先取得 module.exports.default,然后同同步组件一致,执行构造子类构造函数:
return isObject(comp) ? base.extend(comp) : comp;将构造后的子类构造函数 Vue.component 赋给 factory.resolved,执行 forceRender:
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:
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 钩子在父组件的 patch 里 invokeInsertHook 中触发。
另外,异步组件的强制更新会引起父组件里的其他子组件执行 updateChildComponent,如果该子组件判断有普通插槽或动态插槽(不包含具名插槽),则会强行渲染包含插槽的子组件:
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context);
vm.$forceUpdate();
}然后在 flushSchedulerQueue 里执行 callUpdatedHooks(updatedQueue) 触发父组件 vue updated 钩子,异步组件加载完成。
本章小结
- 本章介绍了
vue执行的Mount阶段中的通过render生成vnode部分。 - 在执行实例化
渲染 watcher时,触发render生成vnode。 - 分析了普通节点
render、同步/异步组件render的过程。