Interviewer: Do you understand Vue's diff algorithm? Tell me
1. What is it?
diff
The algorithm is an efficient algorithm for comparing tree nodes at the same level
It has two characteristics:
- Comparisons will only be performed at the same level, not cross-level comparisons
- In the process of diff comparison, the loop compares from both sides to the middle
diff
Algorithms are applied in many scenarios. vue
In , it acts on the comparison of old and new nodes dom
rendered from virtual to dom
realVNode
2. Comparison method
diff
The overall strategy is: depth first, same layer comparison
- Comparisons will only be performed at the same level, not cross-level comparisons
- During the comparison, the loop is closed from both sides to the middle
Here is an example of an vue
algorithm diff
update:
The old and new VNode
nodes are shown in the figure below:
After the first cycle, it is found that the old node D is the same as the new node D, and the old node D is directly reused as the diff
first real node, and the old node endIndex
is moved to C, and the new node is startIndex
moved to C
After the second cycle, the end of the old node is also the same as the beginning of the new node (both are C). Similarly, the diff
real node of C created later is inserted behind the node of D created for the first time. At the same time, the old node endIndex
moves to B, and the new node startIndex
moves to E
In the third cycle, it is found that E is not found. At this time, a new real node E can only be created directly and inserted after the C node created for the second time. At the same time the new node startIndex
moves to A. startIndex
The and of the old node endIndex
remain unchanged
In the fourth cycle, it is found that the beginnings of the old and new nodes (both are A) are the same, so the real node of A is created diff
later , and inserted behind the E node created last time. At the same time, the old node startIndex
moves to B, and the new node startIndex
moves to B
In the fifth cycle, the situation is the same as the fourth cycle, so the B real node created diff
later inserted behind the A node created last time. At the same time, the old node startIndex
moved to C, and the startIndex of the new node moved to F
The new node startIndex
is already greater endIndex
than , you need to newStartIdx
create newEndIdx
all the nodes between and , that is, node F, directly create the real node corresponding to the F node and put it behind the B node
3. Principle Analysis
When the data changes, set
the method will be called to Dep.notify
notify all subscribers Watcher
, and the subscribers will call patch
to patch the real DOM
one and update the corresponding view
Source location: src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
// 没有新节点,直接执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁及旧节点,根据新节点生成dom元素
if (isRealElement) {
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
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
patch
The first two parameters of the function are oldVnode
and Vnode
, which represent the new node and the old node respectively, and mainly make four judgments:
- There is no new node, directly trigger
destory
the hook of the old node - There are no old nodes, which means that it is when the page is just initialized. At this time, there is no need to compare at all, and all are newly created, so only call
createElm
- The old node is the same as the new node itself, by
sameVnode
judging whether the nodes are the same, if they are the same, directly callpatchVnode
to process the two nodes - The old node is different from the new node itself. When the two nodes are different, directly create a new node and delete the old node
The following is mainly about patchVnode
the part
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
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
}
// 如果新旧都是静态节点,并且具有相同的key
// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
// 也不用再有其他操作
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)
}
// 如果vnode不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新vnode和老vnode是文本节点或注释节点
// 但是vnode.text != oldVnode.text时,只需要更新vnode.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)
}
}
patchVnode
Mainly made several judgments:
- Whether the new node is a text node, if so,
dom
the text content of the direct update is the text content of the new node - If both the new node and the old node have child nodes, then compare and update the child nodes
- Only the new node has child nodes, and the old node does not, so there is no need to compare, all nodes are brand new, so just create all new ones directly, new creation refers to creating all new nodes and adding them to the parent
DOM
node - Only the old node has child nodes but the new node does not, which means that all the old nodes are gone in the updated page, so what to do is to delete all the old nodes, that is, delete
DOM
directly
If the child nodes are not exactly the same, callupdateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
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
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点
if (isUndef(idxInOld)) {
// New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的key的新节点是否是同一个节点
//不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
while
The loop mainly handles the following five scenarios:
- When the new and old
VNode
nodesstart
are the same, directlypatchVnode
, and the start index of the new and oldVNode
nodes are both increased by 1 - When the old and new
VNode
nodesend
are the same, it is also directpatchVnode
, and at the same time the end index of the old and newVNode
nodes are both reduced by 1 - When the of the old
VNode
node is the same as that ofstart
the newVNode
nodeend
, at thispatchVnode
time , the current realdom
node needs to be movedoldEndVnode
to the back of , at the same time,VNode
the start index of the old node is increased by 1, and the end index of the newVNode
node is decreased by 1 - When the of the old
VNode
node is the same as that ofend
the newVNode
nodestart
,patchVnode
after , the current realdom
node needs to be movedoldStartVnode
to the front of , andVNode
the end index of the old node is decreased by 1, and the start index of the newVNode
node is increased by 1 - If none of the above four situations are met, it means that there are no identical nodes that can be reused, and it will be divided into the following two situations:
- Find the
VNode
old node that is consistentkey
value is the value of and the correspondingindex
sequence value, and then proceed , and at the same time move to the front of the corresponding realvalue
newStartVnode
key
VNode
patchVnode
dom
oldStartVnode
dom
- Call to
createElm
create a newdom
node and put it at thenewStartIdx
current position
- Find the
summary
- When the data changes, the subscriber
watcher
will callpatch
the realDOM
patch - By
isSameVnode
making a judgment, if it is the same, callpatchVnode
the method patchVnode
did the following:- Find the corresponding truth
dom
, calledel
- If both have text nodes and are not equal, set
el
the text node toVnode
the text node - If
oldVnode
there is a child node andVNode
there is no child node, deleteel
the child node - If there is
oldVnode
no child nodeVNode
, thenVNode
the child node will be realized and added toel
- If both have child nodes, execute
updateChildren
function compare child nodes
- Find the corresponding truth
updateChildren
Mainly did the following operations:- Set the old and new
VNode
head and tail pointers - Compare the old and new head and tail pointers, move closer to the middle in a loop, call to repeat the process according to the situation
patchVnode
,patch
call tocreateElem
create a new node, find akey
consistentVNode
node from the hash table, and then divide the situation into operations
- Set the old and new
references
- https://juejin.cn/post/6881907432541552648#heading-1
- https://www.infoq.cn/article/udlcpkh4iqb0cr5wgy7f