1. What is it?
The diff 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
The algorithm is applied in many scenarios, in vue
which it is used to compare the old and new nodes that are virtual dom
rendered into real ones dom
VNode
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 B node 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 sum of the old nodes endIndex
remains 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 diff
real node of A is created 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 diff
the B real node created later is 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 larger than that, and all nodes between and need endIndex
to be created , that is, node F, and the real node corresponding to the F node is directly created and placed behind the B nodenewStartIdx
newEndIdx
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
sum Vnode
, which represent the new node and the old node respectively. Four judgments are mainly made:
- 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, directly
DOM
delete
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 old and new
VNode
nodesstart
are the same, directly , and the start index of thepatchVnode
old and new nodes are both increased by 1VNode
- When the old and new
VNode
nodesend
are the same, it is also direct , and the end indexes of thepatchVnode
old and new nodes are both reduced by 1VNode
- When the old
VNode
node is the same asstart
the newVNode
node , the current real node needs to be moved to the back at this time. At the same time, the start index of the old node is increased by 1, and the end index of the new node is decreased by 1.end
patchVnode
dom
oldEndVnode
VNode
VNode
- When the old
VNode
node is the same asend
the newVNode
nodestart
, at this timepatchVnode
, the current real node needs to bedom
moved tooldStartVnode
the front, andVNode
the end index of the old node is reduced by 1, andVNode
the start index of the new 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 same old node from the hash table with the old value as the value and the corresponding sequence as the value,
VNode
and then proceed , and at the same time move this real to the front of the corresponding realkey
index
value
newStartVnode
key
VNode
patchVnode
dom
oldStartVnode
dom
- Call
createElm
to create a newdom
node and place it at the currentnewStartIdx
position
- Find the same old node from the hash table with the old value as the value and the corresponding sequence as the value,
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 new and old 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