源码笔记(六):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
的过程。