patch
當(dāng)我們通過createComponent
創(chuàng)建了組件VNode
浑槽,接下來會走到vm._update
窍箍,執(zhí)行vm.__patch__
去把VNode
轉(zhuǎn)換成真正的DOM節(jié)點土全。但是針對一個普通的VNode
節(jié)點报嵌,接下來我們來看看組件的VNode
會有哪些不一樣的地方旷祸。
patch
的過程會調(diào)用createElm
創(chuàng)建元素節(jié)點糜颠,回顧一下createElm
的實現(xiàn)汹族,它的定義在src/core/vdom/patch.js
中:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// ...
}
createComponent
我們刪掉多余的代碼,只保留關(guān)鍵的邏輯其兴,這里會判斷createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值顶瞒,如果為true
則直接結(jié)束,那么接下來看一下createComponent
方法的實現(xiàn):
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
createComponent
函數(shù)中元旬,首先對 vnode.data
做了一些判斷:
let i = vnode.data
if (isDef(i)) {
// ...
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
// ...
}
// ..
}
如果vnode
是一個組件VNode
榴徐,那么條件會滿足守问,并且得到i
就是init
鉤子函數(shù),我們在創(chuàng)建組件VNode
的時候合并鉤子函數(shù)中就包含init
鉤子函數(shù)坑资,定義在src/core/vdom/create-component.js
中:
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
init
鉤子函數(shù)執(zhí)行也很簡單耗帕,我們先不考慮keepAlive
的情況,它是通過createComponentInstanceForVnode
創(chuàng)建一個Vue
的實例袱贮,然后調(diào)用$mount
方法掛載子組件仿便, 先來看一下createComponentInstanceForVnode
的實現(xiàn):
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
): Component {
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
createComponentInstanceForVnode
函數(shù)構(gòu)造的一個內(nèi)部組件的參數(shù),然后執(zhí)行new vnode.componentOptions.Ctor(options)
字柠。這里的vnode.componentOptions.Ctor
對應(yīng)的就是子組件的構(gòu)造函數(shù)探越,它實際上是繼承于Vue
的一個構(gòu)造器Sub
,相當(dāng)于new Sub(options)
這里有幾個關(guān)鍵參數(shù)要注意幾個點窑业,_isComponent
為true
表示它是一個組件钦幔,parent
表示當(dāng)前激活的組件實例。
所以子組件的實例化實際上就是在這個時機執(zhí)行的常柄,并且它會執(zhí)行實例的_init
方法鲤氢,這個過程有一些和之前不同的地方需要挑出來說,代碼在src/core/instance/init.js
中:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
這里首先是合并options
的過程有變化西潘,_isComponent
為true
卷玉,所以走到了initInternalComponent
過程,這個函數(shù)的實現(xiàn)也簡單看一下:
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
const opts = vm.$options = Object.create(vm.constructor.options)
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
這個過程我們重點記住以下幾個點即可:opts.parent = options.parent
喷市、opts._parentVnode = parentVnode
相种,它們是把之前我們通過createComponentInstanceForVnode
函數(shù)傳入的幾個參數(shù)合并到內(nèi)部的選項$options
里了。
再來看一下_init
函數(shù)最后執(zhí)行的代碼:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
由于組件初始化的時候是不傳el
的品姓,因此組件是自己接管了$mount
的過程寝并,回到組件init
的過程,componentVNodeHooks
的init
鉤子函數(shù)腹备,在完成實例化的_init
后衬潦,接著會執(zhí)行child.$mount(hydrating ? vnode.elm : undefined, hydrating)
。這里hydrating
為true
一般是服務(wù)端渲染的情況植酥,我們只考慮客戶端渲染镀岛,所以這里$mount
相當(dāng)于執(zhí)行child.$mount(undefined, false)
,它最終會調(diào)用mountComponent
方法友驮,進而執(zhí)行vm._render()
方法:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
// ...
}
// set parent
vnode.parent = _parentVnode
return vnode
}
我們只保留關(guān)鍵部分的代碼漂羊,這里的_parentVnode
就是當(dāng)前組件的父VNode
,而render
函數(shù)生成的vnode
當(dāng)前組件的渲染vnode
喊儡,vnode
的parent
指向了_parentVnode
拨与,也就是vm.$vnode
,它們是一種父子的關(guān)系艾猜。
我們知道在執(zhí)行完vm._render
生成VNode
后买喧,接下來就要執(zhí)行vm._update
去渲染VNode
了。來看一下組件渲染的過程中有哪些需要注意的匆赃,vm._update
的定義在src/core/instance/lifecycle.js
中:
export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// 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
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
_update
過程中有幾個關(guān)鍵的代碼淤毛,首先vm._vnode = vnode
的邏輯,這個vnode
是通過vm._render()
返回的組件渲染VNode
算柳,vm._vnode
和vm.$vnode
的關(guān)系就是一種父子關(guān)系低淡,用代碼表達(dá)就是vm._vnode.parent === vm.$vnode
。還有一段比較有意思的代碼:
export let activeInstance: any = null
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// ...
const prevActiveInstance = activeInstance
activeInstance = vm
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// ...
}
這個activeInstance
作用就是保持當(dāng)前上下文的Vue
實例瞬项,它是在lifecycle
模塊的全局變量蔗蹋,定義是export let activeInstance: any = null
,并且在之前我們調(diào)用createComponentInstanceForVnode
方法的時候從lifecycle
模塊獲取囱淋,并且作為參數(shù)傳入的猪杭。因為實際上JavaScript是一個單線程,Vue
整個初始化是一個深度遍歷的過程妥衣,在實例化子組件的過程中皂吮,它需要知道當(dāng)前上下文的Vue
實例是什么,并把它作為子組件的父Vue
實例税手。之前我們提到過對子組件的實例化過程先會調(diào)用initInternalComponent(vm, options)
合并options
蜂筹,把parent
存儲在vm.$options
中,在$mount
之前會調(diào)用initLifecycle(vm)
方法:
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
可以看到vm.$parent
就是用來保留當(dāng)前vm
的父實例芦倒,并且通過parent.$children.push(vm)
來把當(dāng)前的vm
存儲到父實例的$children
中艺挪。
在vm._update
的過程中,把當(dāng)前的vm
賦值給activeInstance
兵扬,同時通過const prevActiveInstance = activeInstance
用prevActiveInstance
保留上一次的activeInstance
麻裳。實際上,prevActiveInstance
和當(dāng)前的vm
是一個父子關(guān)系周霉,當(dāng)一個vm
實例完成它的所有子樹的patch
或者update
過程后掂器,activeInstance
會回到它的父實例,這樣就完美地保證了createComponentInstanceForVnode
整個深度遍歷過程中俱箱,我們在實例化子組件的時候能傳入當(dāng)前子組件的父Vue
實例国瓮,并在_init
的過程中,通過vm.$parent
把這個父子關(guān)系保留狞谱。
那么回到_update
乃摹,最后就是調(diào)用__patch__
渲染VNode
了。
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// ...
}
// ...
}
之前分析過負(fù)責(zé)渲染成DOM的函數(shù)是createElm
跟衅,注意這里我們只傳了2個參數(shù)孵睬,所以對應(yīng)的parentElm
是undefined
。我們再來看看它的定義:
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
// ...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
// ...
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
注意伶跷,這里我們傳入的vnode
是組件渲染的vnode
掰读,也就是我們之前說的vm._vnode
秘狞,如果組件的根節(jié)點是個普通元素,那么vm._vnode
也是普通的vnode
蹈集,這里createComponent(vnode, insertedVnodeQueue, parentElm, refElm)
的返回值是false
烁试。接下來的過程就是,先創(chuàng)建一個父節(jié)點占位符拢肆,然后再遍歷所有子VNode
遞歸調(diào)用createElm
减响,在遍歷的過程中,如果遇到子VNode
是一個組件的VNode
郭怪,則重復(fù)本節(jié)開始的過程支示,這樣通過一個遞歸的方式就可以完整地構(gòu)建了整個組件樹。
由于我們這個時候傳入的parentElm
是空鄙才,所以對組件的插入颂鸿,在createComponent
有這么一段邏輯:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
// ....
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
// ...
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
在完成組件的整個patch
過程后,最后執(zhí)行insert(parentElm, vnode.elm, refElm)
完成組件的DOM插入咒循,如果組件patch
過程中又創(chuàng)建了子組件据途,那么DOM的插入順序是先子后父。
總結(jié)
那么到此叙甸,一個組件的VNode
是如何創(chuàng)建颖医、初始化、渲染的過程也就介紹完畢了裆蒸。我們知道編寫一個組件實際上是編寫一個JavaScript對象熔萧,對象的描述就是各種配置,之前我們提到在_init
的最初階段執(zhí)行的就是merge options
的邏輯僚祷,那么我們從源碼角度來分析合并配置的過程佛致。