function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// 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
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
【Vue源码系列】:DOM-Diff
前言
为了降低直接操作真实DOM带来的性能消耗。Vue内部引入了Vdom(虚拟DOM)。Vdom概念也比较简单,可以看成一个普通的JS对象,用来描述用户界面。而DOM-Diff的过程,简单来说,就是当有数据更新时,首先需要通过JS计算出Vdom的变化,然后再将变化更新到真实的用户界面。接下来,我们从源码出发,逐步分析。
_update
从响应式原理学习中,了解了数据更新时重新执行render函数再次生成新的VNode的原理。但是,真正完成视图界面的更新,还需要经过后续复杂的过程,而这个过程的入口从
vm.update()
开始。流程图如下:观察上面流程图发现,
update()
会将由render()
生成的新VNode与旧VNode交给一个函数patch
去处理。另外,交给
patch
处理之前,update() 会完成一些预处理,步骤如下:const prevVnode = vm._vnode
vm._vnode = vnode
patch
函数,直接遍历 newVNode,为每个节点生成真实DOM,并挂载到每个节点的elm
属性上。patch
函数,对 oldVNode 和newVNode进行对比,找出差异变化,最后完成真实DOM的最小化更新,并且保证 newVNode上每个节点对应着正确的真实DOM。__patch__
patch
的作用:通过比较新旧VNode,找出差异变化,最后完成真实DOM的最小化更新,这个过程也就是Diff
过程。diff核心
我们带着上面的讲到的算法核心,一步步体会。下面重点学习下 具体的diff原理。
sameVnode(节点是否相同)
diff原理实际上就是VNode节点之间比较的过程。首先明确一个概念,两个VNode节点相等的条件:
判断节点是否相等的详细逻辑如下。源码位置:
core/vdom/patch.js
的 sameVnode 函数:patch (更新、删除、创建节点)
接下来,从 diff入口 -
patch
函数开始分析。源码位置:/core/vdom/patch.js
。分析主干逻辑,
patch
主要处理以下内容:若 oldVNode 存在, newVNode 不存在。 则销毁元素。
若 oldVNode 不存在,newVNode 存在。 则创建元素,按照当前虚拟节点创建真实DOM,并挂载到
vnode.elm
。若 oldVNode 和 newVNode 都存在,并且通过 sameVnode 函数 判断两者是否相等。相等,则执行后续进一步比较(自身和子节点),这部分内容是通过
patchVnode
函数处理。稍后我们详细了解。最后,返回diff渲染后的真实
vnode.elm
。接下来,看下
patchVnode
函数的主干逻辑。patchVnode (更新节点)
源代码如下:
patchVnode,主干逻辑:
oldVNode.elm
关联到newVNode.elm
上,使 newVnode 具有对应真实DOM的引用。updateChildren (更新子节点)
对子节点的比较,是diff算法的关键。为了提升渲染效率。子元素集合diff的基本原则, 如下:
具体实现,通过源码可知:比较时,会分别为新旧子节点集合设置头尾两个指针,头尾指针根据「比较规则」,向中间移动,依次比较新旧各个子节点,并更新(修改、移动、创建、删除)对应的真实DOM。
diff原理图
【子节点diff前】
【子节点diff后】
diff比较规则
结合上面原理图,整理diff比较规则如下:
patchVnode
、patchVnode
patchVnode
patchVnode
sameVnode
函数判断】sameVnode
函数判断匹配的旧节点和 头(新)是否「相同」patchVnode
函数开发示例
列表渲染 —— key值不可缺
示例代码
图示
列表反转 —— 不加 key
列表反转 —— 加 key
头部添加 —— 不加 key
头部添加 —— 加 key
总结
通过分析源码,可以清晰了解Vue中 DOM-Diff的过程、DOM-Diff的核心:深度优先,同层比较。并结合开发示例,学习了在列表渲染中,添加 key 值可以有效的复用已存在的DOM,提升渲染效率。
交流
如果这篇文章帮助到你,点赞和关注不失联,你的支持是对笔者最大的鼓励!
微信关注 「 乘风破浪大前端 」,发现更多有趣好用的前端知识和实战。
干货系列文章汇总如下,欢迎 start 、follow 交流学习👏🏻。
关于本文如有任何意见或建议,欢迎评论区讨论和指正。
也许你还想看: