1. Vue2 中虚拟 DOM Diff 原理
// 最主要的三个函数,其他函数暂不研究
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (__DEV__) {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx))
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
)
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
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 (__DEV__) {
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)
}
}
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
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 (__DEV__) {
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
}
总结
初始化渲染阶段不会经历虚拟 DOM Diff 算法,更新阶段才会。
- 更新阶段开始
- 更新数据时,会触发数据代理的 set,数据代理的 set 会更新原 data 数据,又触发数据劫持的 set,数据劫持的 set 会同步更新数据,同时调用 dep.notify() 方法,dep.notify() 方法会遍历 dep 保存的所有 watcher,将其添加异步队列,等待将来更新;
- 等 js 主线程执行完所有同步代码,就会执行异步代码,此时会取出所有watcher,最终调用 updateComponent 方法进行更新,更新过程会通过 vm._render() 方法得到新的虚拟 DOM 树,简称为“新树”(初始化渲染生成的是旧的虚拟 DOM 树,简称为“旧树”),通过 vm._update() 来进行虚拟 DOM Diff 算法流程
- 虚拟 DOM Diff 算法目的:将页面 DOM 元素(目前和旧树一致),更新成新树的样子
- 首先调用
patch方法
patch
- 判断新树是否存在,不存在直接删除旧树所有 DOM 节点
- 判断旧树是否存在,不存在直接创建所有新树的节点进行渲染
- 如果新旧树都存在,并且根节点是相似节点,使用
patchVnode对两根节点进行进一步比较
patchVnode
- 如果新旧元素一样,直接return
- 如果是静态节点、key相同、克隆节点、v-once节点等,直接将新节点改为旧节点即可,无需变化
- 如果新节点没有文本内容(有文本内容就是文本节点,没有文本内容就是元素节点)说明是元素节点。
-
- 判断新旧节点是否都有子节点,并且所有子节点是否相同,如果不同,调用
updateChildren更新子节点 - 判断新节点是否有子节点,如果有,说明旧的没有,将新节点所有子节点添加上去
- 判断旧节点是否有子节点,如果有,说明新的没有,删除旧节点所有子节点
- 新旧节点都没有子节点,判断旧节点是否是文本节点,如果是清空文本内容
- 判断新旧节点是否都有子节点,并且所有子节点是否相同,如果不同,调用
- 如果新节点有文本内容,是文本节点,但是新旧节点文本内容不一样,就更新成新的文本内容
updateChildren
- 整体是从两端到中间遍历,一一比较子节点是否相同,从而复用或更新相应节点
-
- 取新树第一个下标,叫做新前下标,新前下标对应的节点,叫做新前节点
- 取新树最后一个下标,叫做新后下标,新后下标对应的节点,叫做新后节点
- 取旧树第一个下标,叫做旧前下标,旧前下标对应的节点,叫做旧前节点
- 取旧树最后一个下标,叫做旧后下标,旧后下标对应的节点,叫做旧后节点
- 遍历判断旧前下标是否小于等于旧后下标并且新前下标是否小于等于新后下标,满足条件遍历开始:
-
- 判断新前节点是否存在,如果不存在旧前下标++
- 判断旧后节点是否存在,如果不存在旧后下标--
- 判断旧前节点和新前节点是否相同,如果相同需要进一步比较(patchVnode),旧前下标、新前下标++
- 判断旧后节点和新后节点是否相同,如果相同需要进一步比较(patchVnode),旧后下标、新后下标--
- 判断旧前节点和新后节点是否相同,如果相同需要进一步比较(patchVnode),将旧前节点插入到旧后节点的后面去,旧前下标++、新后下标--
- 判断旧后节点和新前节点是否相同,如果相同需要进一步比较(patchVnode),将旧后节点插入到旧前节点的前面去,旧后下标--、新前下标++
- 如果都不相等,看元素的 key 属性
-
-
- 提取旧树子元素的key属性,判断新前节点的key在不在旧树key中
- 如果不在,创建新元素插入进去
- 如果在,需要进一步比较(patchVnode),然后再将 key 对应元素插入到旧前节点前面去
- 新前下标++
-
- 遍历结束判断旧前下标是否大于旧后下标
-
- 如果大于,说明旧树遍历完了,新树还有剩余节点,需要添加新树剩余节点
- 判断新前下标是否大于新后下标
-
- 如果大于,说明新树遍历完了,旧树还有剩余节点,需要将旧树剩余节点删除掉
2. Vue3 中虚拟 DOM Diff 原理
// diff 算法中比较相同层级的拥有key的子节点
// c1:旧的虚拟DOM节点(旧树)
// c2:新的虚拟DOM节点(新树)
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// i 遍历的起始标识
let i = 0
// l2 新树节点总长度
const l2 = c2.length
// e1 旧树最大下标
let e1 = c1.length - 1 // prev ending index
// e2 新树最大下标
let e2 = l2 - 1 // next ending index
// 整个diff分为5个流程,分别处理5种不同情况
// 1. 从前到后:整体从前到后一一对比,找到相同的节点复用&更新,不同节点交给下一步处理
// (a b) c
// (a b) d
while (i <= e1 && i <= e2) {
// n1 旧树对应i的节点
const n1 = c1[i]
// n2 新树对应i的节点
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 判断n1和n2是否相同:判断依据为元素类型是否相同(type)和元素key属性是否相同(key)
if (isSameVNodeType(n1, n2)) {
// 如果相同,就要比较内部的细节,继续打补丁
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 如果不相同直接退出整个循环,不比较了
break
}
i++
}
// 2. 从后到前:整体从后到前一一对比,找到相同的节点复用&更新,不同节点交给下一步处理
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
break
}
e1--
e2--
}
// 3. 新节点更多,将新节点添加上去
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
// 4. 旧节点更多,将旧节点给卸载
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. 乱序情况
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 建立map容器,内部保存新树元素的key和对应下标的映射关系
// Map<newKey, newIndex>
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
warn(
`Duplicate keys found during update:`,
JSON.stringify(nextChild.key),
`Make sure keys are unique.`
)
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 遍历找打旧元素和新元素的映射关系
// Map<newIndex, oldIndex>
// newIndex是新元素对应的位置(从0开始)
// oldIndex是旧元素对应的下标(+1)
// matching nodes & remove nodes that are no longer present
let j
let patched = 0 // 已经打补丁的数量(已经处理好的元素数量)
const toBePatched = e2 - s2 + 1 // 一共需要打补丁的数量(总共需要处理的元素数量)
let moved = false
// used to track whether any node has moved
let maxNewIndexSoFar = 0
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
// 定义一个新的容器,实际是数组,但是看做Map容器使用
// Map<newIndex, oldIndex>
const newIndexToOldIndexMap = new Array(toBePatched)
// 给newIndexToOldIndexMap创建并赋值为0
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
// 遍历旧树剩下的元素
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
// 如果已经打补丁的数量大于等于一共需要打补丁的数量
// 说明旧树多于新树,要删除旧树多余的元素
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
// 如果旧树元素有key
if (prevChild.key != null) {
// 得到对应新树元素的下标
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 如果旧树元素没有key
// 也要去新树中找有没有也是没有key的元素和它对应的下标
// key-less node, try to locate a key-less node of the same type
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
// 如果没找到了新树元素的下标,说明旧树元素不存在
if (newIndex === undefined) {
// 删除旧树元素
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 找到了新树元素下标,建立Map<newIndex, oldIndex>
// 需要注意的是newIndex从0开始,oldIndex会递增1,因为0有特殊含义
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 对新旧元素进行打补丁
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 5.3 移动元素位置,渲染
// generate longest stable subsequence only when nodes have moved
// 获取最长递增子序列的下标(详见最后), 对应下标元素不动,其他元素移动
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// mount new
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
// 获取最长递增子节点的下标
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0] // 存储最长递增子节点的下标
let i, j, u, v, c
const len = arr.length
// 1. 遍历所有
for (i = 0; i < len; i++) {
// 取出对应i的元素
const arrI = arr[i]
// 判断不等于0,等于0就直接跳过该元素了,说明当前节点是空的
if (arrI !== 0) {
// j 是 result 中最后一个元素
j = result[result.length - 1]
// 通过 arr[j] 取出最后一个元素,对应arr中的值
// 判断result最后一个元素对应的值,是否小于当前遍历的元素
if (arr[j] < arrI) {
// 如果小于就+进来,遍历继续
p[i] = j
result.push(i)
continue
}
// 如果不小于,使用二分查找,找到对应的元素的下标
u = 0
v = result.length - 1
while (u < v) {
c = (u + v) >> 1 // 取中间值, 类似于:Math.floor((u + v) / 2)
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
diff 会遍历所有子节点,分为5个流程处理
- 从前到后一一对比,找到相同的节点复用&更新,不同节点交给下一步处理
- 从后到前一一对比,找到相同的节点复用&更新,不同节点交给下一步处理
- 旧节点没了,还有新节点,将新节点添加上去
- 新节点没了,还有旧节点,将旧节点删除
- 还有元素,说明是新旧节点都还有,并且是乱序的,此时分为三步处理
- 创建 map 容器,内部保存新树元素的key和对应下标的映射关系
- 创建 array 容器(实际当做 map 容器使用),内部保存新树元素下标和旧树元素下标的映射关系
-
- 新树元素下标,会从0开始
- 旧树元素下标会在原有的基础上+1
- 获取子节点最长递增子序列的下标, 对应下标元素不动,其他元素移动
到此完成所有子节点元素的更新