这是一个系列文章,请关注 vue@2.6.11 源码分析 专栏
前情回顾
new Vue(..)
之后总共有两个大的步骤,第一步是调用vm._init
完成组件的各种准备(初始化)工作,然后是开始结合数据与模板实现页面的渲染。vue引入了虚拟DOM技术,这里页面渲染分为两步,将模板和数据(转为了render函数)转为虚拟DOM树,而后再将虚拟DOM树同步到界面上。上一小节已经分析过创建虚拟DOM树的过程(vm._render
),现在我们来看看虚拟DOM是如何更新到界面上的,这个过程在vm._update
方法上。
updateComponent = () => {
vm._update(vm._render(), hydrating) // hydrating: ssr相关 忽略
}
new Watcher(vm, updateComponent, noop, {
before () { /*...*/ }
}, true /* isRenderWatcher */)
vm._update
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(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)
}
restoreActiveInstance()
// 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.
}
//...
}
入参vnode
是来自上一步_render
刚创建的虚拟DOM树,preVnode
则是上一次创建的虚拟DOM树,prevEl
是上一次组件渲染的根DOM。注意更新了vm._vnode = vnode
然后就是最关键的步骤patch
,对比新老虚拟DOM树,找出差异,同步到界面上。patch方法会返回一个DOM,然后保存到$el
上,同时也看到后面有更新 $el.__vue__ = vm
,这样组件就和实际渲染内容的根DOM相互关联起来了。
HOC场景的 $parent.$el
更新?❎ 先遗留
下面重点看下patch方法
patch:组件diff入口
在vue当前版本中,该方法的入口总是组件去调用,因为这个方法的定义Vue.prototype
上,所以只有vue实例(就是组件)可以调用。
Vue.prototype.__patch__ = inBrowser ? patch : noop
这里的核心逻辑在snabbdom
流程上是类似的,参考专栏snabbdom@3.5.1 源码分析第三篇。还是看下vue@2.6.11
这里的实现。
export function createPatchFunction (backend) {
//...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
//...
if (isUndef(oldVnode)) {
//...createElm
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
//... patchVnode
} else {
//... createElm
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
总共分为四种情况,见下面分析。
!vnode && oldVnode
(vnode不存在但是oldVnode存在)vnode && !oldVnode
(vnode存在 并且 oldVnode不存在)vnode && oldVnode
(二者均存在)oldVnode
不是DOM
&&sameVnode(oldVnode, vnode)
oldVnode
是DOM
||!sameVnode(oldVnode, vnode)
1. !vnode && oldVnode(vnode不存在但是oldVnode存在)
直接调用invokeDestroyHook
触发oldVnode
销毁逻辑
invokeDestroyHook
function invokeDestroyHook (vnode) {
let i, j
const data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
}
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
}
触发destroy
相关钩子:vnode.data.hook.destroy
(针对组件placeholder vnode
的钩子)、cbs.destroy
(cbs收集的是模块上的钩子回调)
看到vnode.data.hook.destroy
的区分了keepAlive场景,普通场景下直接调用组件实例的$destroy
方法(注意不是开发者提供的destroyed
的生命周期方法)。至于deactivateChildComponent
后面可能会单独小节分析keep-alive
组件,这里暂时忽略。
const componentVNodeHooks = {
//... init、insert、prepatch
destroy (vnode: MountedComponentVNode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
}
下面看下vm.$destroy
做了些什么
vm.$destroy()
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
- 如果组件正在销毁则返回。
- 触发生命周期:
beforeDestroy
,并设置正在销毁_isBeingDestroyed
标识 - 取消父子组件关系(
parent.$children
中移除当前删除的组件实例vm) - watcher销毁
- 销毁渲染关联的
watcher
(在mountComponent
创建的,用来渲染组件的) - 销毁组件中开发者提供
watch
属性生成的watchers
(在initState
->initWatcher
中创建的)
- 销毁渲染关联的
- 设置已经销毁标识
_isDestroyed
- 移除
vm._vnode
,同样是通过patch
去处理,通过和null
进行diff
,来移除(和snabbdom
几乎一致的思路) - 触发
destroyed
(开发者提供的)声明周期 - 取消所有的事件监听
- 取消关联的DOM指向js对象的引用,
vm.$el.__vue__ = null
- ❎ 否则,DOM删除不了导致内存泄漏?需要验证下 参考
- vm.$vnode.parent = null,keep-alive场景下的内存泄漏:issue#6759
我学习到的:需要清理一切需要清理的,并且所有的属性最好都是统一在一个地方声明,确保删除的时候没有遗漏。
小结
vm.$destroy
:组件销毁包括DOM移除、事件和watcher
等移除、触发beforeDestroy
、destroyed
生命周期等。cbs.destroy
收集注册的module的对应钩子,即在调用createPatchFunction
传递的modules
参数(如果你看过snabbdom源码分析,就会知道他们的作用啦)。
2. vnode && !oldVnode(vnode存在 并且 oldVnode不存在)
这种情况暂时不会真正挂载界面上,因为没有提供挂载点。这种情况就两个步骤,如下:
// empty mount (likely as component), create new root element
isInitialPatch = true // 关键
createElm(vnode, insertedVnodeQueue) // 注意:没有提供挂载点:parentElm
// isInitialPatch为true时,会延迟insert hooks执行,指导真正挂载到界面上。
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // isInitialPatch: true
首先调用createElm
来创建DOM并挂载,非常重要下面会重点分析。invokeInsertHook
,注意这里在调用invokeInsertHook
时设置了isInitialPatch = true
,逻辑如下分析。
invokeInsertHook
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
注意isInitialPatch
情况,延迟执行queue中虚拟DOM的data.hook.insert
的执行,并保存到 vnode.parent.data.pendingInsert
中。等真正会进行挂载的时候,采取触发。
如果不是上面场景,即会进行DOM的挂载,执行vnode.data.hook.insert
。注意,执行componentVNodeHooks
中的hook
的vnode
一定是组件的placeholder vnode
,因为这些hook就在创建组件placeholder vnode
时安装的。
其中vnode.data.hook.insert
如下:
const componentVNodeHooks = {
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
//...keepAlive场景,暂且忽略
},
//...
}
如果组件已经没有挂载过,初次挂载,则触发mounted
生命周期,并设置_isMounted标识组件已经挂载。
3. vnode && oldVnode(二者均存在)
3.1 oldVnode不是DOM && sameVnode(oldVnode, vnode)
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) // isInitialPatch: false
两步:patchVnode
-> insertInvokeHook
。patchVnode
很重要,后面会单独分析,其目的就是对新老两个虚拟DOM进行对比,前提是这两个虚拟DOM被判断为samveVnode
,才有意义。
另外看下sameVnode
的实现,如下。逻辑很简单,没什么好说的,注意针对异步加载情况单独加了判断。另外undefined === undefined
为true
,所以新老节点都没有设置的条件(如key
)会略过,因为是true
啊。
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
3.2 oldVnode是DOM || !sameVnode(oldVnode, vnode)
if (isRealElement) {
// ... ssr 相关
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 第三个参数:issue##4590
createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm))
// update parent placeholder node element, recursively
// ...
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
- 如果
oldVnode
是一个DOM,则创建一个真实的VNode,然后替换oldVnode
变量(不会创建孩子的虚拟DOM) createELm
,根据vnode树创建DOM树,并挂载到界面上- update parent placeholder node element, recursively
// component root element replaced. // 这是之前版本的注释,解释了当前场景的背景
// update parent placeholder node element, recursively
大概知道原因了,首先这里不是组件移除,而是组件的根节点被替换的场景。看起来是组件的根节点被替换的时候需要做一些特殊处理,先cbs.destroy
,再cbs.create
,显然和模块有关。猜测模块测create
和destroy
钩子主要用来处理组件placeholder vnode
(源码注释中的placeholder node element
)和组件根vnode
(源码注释中的component root element
)关系的。先遗留❎,后续分析模块时,再验证。
另外这里还涉及两个issue:issue#6718、issue#6513。看起来是这种场景特殊case的处理。
另外再看下 isPatchable
方法的含义,fix patch modules error on empty component root
首先这里的vnode是组件的根vnode,isPatchable
用来判断递归根孩子组件中最后一个根孙子组件的根节点的tag是否存在。假设当前组件是component-a
,则isPatchable()
会返回false
。所以这个函数的传达的含义是这个vnode是否可以patch?。
<!-- component-a组件的template -->
<component-b></component-b> <!-- 该组件的根节点是一个组件-->
<!-- component-b 组件的template -->
</component-c> <!-- 该组件的根节点是一个组件 -->
<!-- component-c 组件的template -->
<component-c>
<> || 文本节点 <!-- 空节点、文本节点 -->
</component-c>
- 移除节点:如果
oldVnode.elm
存在,则调用removeVnodes
删除oldVnode.elm
;否则(说明是oldVnode
是组件placeholder vnode
),针对组件则调用invokeDestroyHook
来处理。
下面单独说一下removeVnodes
removeVnodes -> xxxInvokeRemoveHook
function removeVnodes (vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}
如果是文本节点,直接removeNode
(dom操作,parent.removeChild(...)
)
否则如果tag
存在,则可能是浏览器内置标签如div
,也可能是组件(如todo-item
))
removeAndInvokeRemoveHook
用来触发remove
相关的钩子、递归处理子组件、删除当前元素rm()
invokeDestroyHook
触发destroy相关的的钩子
removeAndInvokeRemoveHook
区分了普通节点和组件节点。
如果是普通节点如div
则直接removeNode
移除就好。
如果是组件的placeholder vnode
则需要触发组件的remove
相关的钩子,并且递归删除组件实际渲染内容的根节点即在vm._render()
返回的虚拟DOM,保存在vm._vnode
上。
function removeAndInvokeRemoveHook (vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
let i
const listeners = cbs.remove.length + 1
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners)
}
// recursively invoke hooks on child component root node
if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm)
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm)
}
if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
i(vnode, rm)
} else {
rm()
}
} else {
removeNode(vnode.elm)
}
}
rm()
是最终删除DOM的方法,注意到该方法有一个listeners
属性,用来保存有多个cbs.remove
,cbs.remove
收集自模块,说明模块监听了该事件,此时会将rm
的调用交给模块,只有当所有的模块执行完成以后,才能真正删除DOM,下面下面remove
方法中if
条件.
function createRmCb (childElm, listeners) {
function remove () {
if (--remove.listeners === 0) {
removeNode(childElm)
}
}
remove.listeners = listeners
return remove
}
小结
根据vnode创建DOM
createElm:根据vnode创建DOM并挂载到树中
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode)
setScope(vnode)
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)
}
}
首先:第一个if
中cloneNode
逻辑,是有特殊场景的,见commit # 956756b1提交里面有test case,这里暂不深究 ❎。
然后(关键):根据placeholder vnode
尝试创建子组件实例并渲染子组件,createComponent
返回true
,说明当前vnode关联的是一个组件,否则进入后面逻辑(非组件情况)。
创建元素并挂载,区分三种情况:tag
存在、isComment
、text vnode
,下面重点看下最常见的场景tag
存在时
- 首先是通过
document.createElement
创建该元素 setScope
是为了支持 scoped CSS. 特性的。 暂遗留 ❎- 调用
createChildren
递归创建孩子function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }
触发create
相关的钩子,并保存新创建到元素到insertVnodeQueue
,后面invokeInsertHooks
时会用到。cbs.create
是数组是因为会存在多个模块需要处理该元素(主体是模块),而vnode.data.hook.create
只是用来处理自身的(主体是自己)
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
最后,将创建的DOM
挂载或者衔接(不一定是挂载到界面上)
小结
补充:
isComment = true
的情况,部分场景可能会调用创建一个空节点,vue中通过注释节点模拟实现。
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
createComponent:根据placeholder vnode
创建(子)组件实例并渲染
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
}
}
}
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {
// keepAlive 场景,暂忽略
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
//... insert、prepatch、destroy
}
首先会调用vnode.data.hook.init
见componentVNodeHooks.init
,不考虑keepAlive场景下,这里会调用createComponentInstanceForVnode
创建子组件实例,而后调用$mount
进行子组件的渲染。和我们之前的文章new Vue() 整体流程对应上了是不是,整个过程两个大的步骤:实例初始化 + 渲染
上面创建完组件实例后,会将组件实例保存到vnode.componentInstance
上,如果存在组件实例后则会调用initComponent
,而后调用insert
方法将组件内容挂载到dom树上。
下面分析下initComponent
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
上面部分介绍patch
的第二个场景vnode && !oldVnode
会创建一个游离组件的场景的处理,当这个遗留组件被利用时(需要挂载到dom树中时),则需要暂遗留将之前存储在vnode.data.pendingInsert
中的insertedVnodeQueue
取出,因此这个阶段需要被应用了。 这种游离组件,具体是什么场景,暂遗留 ❎。
然后就是,将根DOM保存到placeholder vnode
.elm上。而后就是触发create相关的钩子,当前版本并未使用vnode.data.hook.create
钩子,然后调用setScope
,scoped css相关,暂遗留。
至于else
里面的分支是处理empty component root
这种特殊场景的,如:fix patch modules error on empty component root、issue#3455:fix ref on empty component root 。看来这个empty component root
会引起很多问题啊,isPatchable
方法就是用来判断是不是empty component root
,如果是则不能patch
,isPatchable
返回false
。
小结
patchVnode & updateChildren
这两个方法和snabbdom中的实现几乎完全一致,可以参考,下面重点说下patchVnode差异部分。
如果是异步组件,则走异步组件加载的处理,return
如果是静态节点,则走静态节点的优化处理:isStatic,编译环节会给静态节点添加该标记。参考官方解释-static-hoisting,目的是对于此类节点在更新时没必要重新构造vnode然后对比。 return
然后如果是组件placeholder vnode
,则通过调用vnode.data.hook.prepatch
实现组件placeholder vnode
的更新。看到在prepatch
方法中调用了updateChildComponent
来进行组件数据(属性,事件等)的更新
// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
},
}
然后触发update
相关的钩子(cbs.update、vnode.data.hook.update)
最后触发vnode.data.hook.postpatch
。搜索src/发现可能和directives有关,后面会单独章节分析指令相关,暂遗留 ❎
这里主要区别是针对组件vnode的处理:updateChildComponent
updateChildComponent
在之前创建的组件实例中,组件vue实例是保存在vnode.componentInstance
中,但是由于属性值、事件等都可能发生了变化,因此需要更新。虽然组件实例不会重新创建,但是组件标签本身关联的place holder vnode
还是会重新创建(新的vnode),并且在_render
-> componentComponent
会获取最新的componentOptions
,保存到vnode.componentOptions
。
因此这里就是将新的vnode.componentOptions
更新到oldVnode.componentInstance
中。
注意:由于这里给vm._props
重新赋值了,因此组件中computed
、watch
、渲染watcher
等订阅的观察者都会被触发。
export function updateChildComponent (
vm: Component, propsData: ?Object, listeners: ?Object,
parentVnode: MountedComponentVNode, renderChildren: ?Array<VNode>) {
//... slot 相关,暂且忽略,后面可能小节分析
vm.$options._parentVnode = parentVnode
vm.$vnode = parentVnode // update vm's placeholder node without re-render
if (vm._vnode) { // update child tree's parent
vm._vnode.parent = parentVnode
}
vm.$options._renderChildren = renderChildren
// update $attrs and $listeners hash
// these are also reactive so they may trigger child update if the child
// used them during render
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
// update props
if (propsData && vm.$options.props) {
toggleObserving(false)
const props = vm._props
const propKeys = vm.$options._propKeys || []
for (let i = 0; i < propKeys.length; i++) {
const key = propKeys[i]
const propOptions: any = vm.$options.props // wtf flow?
props[key] = validateProp(key, propOptions, propsData, vm)
}
toggleObserving(true)
// keep a copy of raw propsData
vm.$options.propsData = propsData
}
// update listeners
listeners = listeners || emptyObject
const oldListeners = vm.$options._parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
//... slot相关 暂且忽略
}
小结
patchVnode
的主要作用是调用updateChildren
来实现节点的复用,另外关键的一点是对于组件的情况调用updateChildComponent
来更新子组件的信息(属性值的重新赋值,事件的重新绑定等),如果子组件的属性发生变更,则会引起子组件的重新渲染。
另外注意的是patchVnode
是针对samveVnode
场景的,即oldVnode
一定可以被复用的情况,因此当然可以在patchVnode
方法中调用updateChildComponent
是合理的,因为此时组件一定会被复用。
总结
组件渲染的整体流程如下: