I wrote an article last year and handwritten a virtual DOM library, so that you can fully understand the diff algorithm and introduce the virtual DOM
process patch
and diff
algorithm process. At that time, the double-ended diff
algorithm was used. This year, I saw Vue3
that the fast algorithm was used diff
, so I also wanted to write an article. Let me record it, but someone must have already written it, so I was wondering if it could be a little different. The last article mainly showed every situation and process of the algorithm step by step by drawing pictures, so I was wondering if it could diff
be Changed to the form of animation, so there is this article. Of course, the current implementation is still based on the double-ended diff
algorithm, and the fast algorithm will be added later diff
.
Portal: Animation demonstration of double-ended Diff algorithm .
The interface is like this. On the left side, you can enter the new and old lists to be compared VNode
, and then click the start button to show the process from beginning to end in the form of animation. On the right are three horizontal lists, representing the old and new VNode
lists respectively. As well as the current real DOM
list, DOM
the list is initially consistent with the old VNode
list, and will be consistent with the new VNode
list after the algorithm ends.
It should be noted that this animation only includes diff
the process of the algorithm, not patch
the process.
Let's first review diff
the function of the double-ended algorithm:
const diff = (el, oldChildren, newChildren) => {
// 指针
let oldStartIdx = 0
let oldEndIdx = oldChildren.length - 1
let newStartIdx = 0
let newEndIdx = newChildren.length - 1
// 节点
let oldStartVNode = oldChildren[oldStartIdx]
let oldEndVNode = oldChildren[oldEndIdx]
let newStartVNode = newChildren[newStartIdx]
let newEndVNode = newChildren[newEndIdx]
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVNode === null) {
oldStartVNode = oldChildren[++oldStartIdx]
} else if (oldEndVNode === null) {
oldEndVNode = oldChildren[--oldEndIdx]
} else if (newStartVNode === null) {
newStartVNode = oldChildren[++newStartIdx]
} else if (newEndVNode === null) {
newEndVNode = oldChildren[--newEndIdx]
} else if (isSameNode(oldStartVNode, newStartVNode)) {
// 头-头
patchVNode(oldStartVNode, newStartVNode)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldStartVNode, newEndVNode)) {
// 头-尾
patchVNode(oldStartVNode, newEndVNode)
// 把oldStartVNode节点移动到最后
el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (isSameNode(oldEndVNode, newStartVNode)) {
// 尾-头
patchVNode(oldEndVNode, newStartVNode)
// 把oldEndVNode节点移动到oldStartVNode前
el.insertBefore(oldEndVNode.el, oldStartVNode.el)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldEndVNode, newEndVNode)) {
// 尾-尾
patchVNode(oldEndVNode, newEndVNode)
// 更新指针
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else {
let findIndex = findSameNode(oldChildren, newStartVNode)
// newStartVNode在旧列表里不存在,那么是新节点,创建插入
if (findIndex === -1) {
el.insertBefore(createEl(newStartVNode), oldStartVNode.el)
} else {
// 在旧列表里存在,那么进行patch,并且移动到oldStartVNode前
let oldVNode = oldChildren[findIndex]
patchVNode(oldVNode, newStartVNode)
el.insertBefore(oldVNode.el, oldStartVNode.el)
// 将该VNode置为空
oldChildren[findIndex] = null
}
newStartVNode = newChildren[++newStartIdx]
}
}
// 旧列表里存在新列表里没有的节点,需要删除
if (oldStartIdx <= oldEndIdx) {
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
removeEvent(oldChildren[i])
oldChildren[i] && el.removeChild(oldChildren[i].el)
}
} else if (newStartIdx <= newEndIdx) {
let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : null
for (let i = newStartIdx; i <= newEndIdx; i++) {
el.insertBefore(createEl(newChildren[i]), before)
}
}
}
The specific implementation steps of this function can refer to the previous article, so this article will not repeat them.
If we want to diff
move this process, we must first find out what objects are animated. Starting from the parameters of the function, first oldChildren
and newChildren
two VNode
lists are essential, which can be represented by two horizontal lists, and then four pointers , which is the key to the double-ended diff
algorithm. We represent it through four arrows, pointing to the current node being compared, and then start the cycle. In fact, the old and new VNode
lists in the cycle basically do not change. What we actually operate is VNode
the corresponding Real DOM
elements, including patch
patching, moving, deleting, adding, etc., so we have another horizontal list to represent the current real list. At the beginning , it must correspond DOM
to the old list. Step by step through the algorithm, it will become and The new list corresponds.VNode
diff
VNode
Let's look back at the function VNode
that creates the object h
:
export const h = (tag, data = {
}, children) => {
let text = ''
let el
let key
// 文本节点
if (typeof children === 'string' || typeof children === 'number') {
text = children
children = undefined
} else if (!Array.isArray(children)) {
children = undefined
}
if (data && data.key) {
key = data.key
}
return {
tag, // 元素标签
children, // 子元素
text, // 文本节点的文本
el, // 真实dom
key,
data
}
}
VNode
The list data we input will be h
created into VNode
objects using functions, so the simplest structure that can be input is as follows:
[
{
tag: 'div',
children: '文本节点的内容',
data: {
key: 'a'
}
}
]
The entered new and old VNode
list data will be saved in store
, which can be obtained in the following ways:
// 输入的旧VNode列表
store.oldVNode
// 输入的新VNode列表
store.newVNode
Next define the relevant variables:
// 指针列表
const oldPointerList = ref([])
const newPointerList = ref([])
// 真实DOM节点列表
const actNodeList = ref([])
// 新旧节点列表
const oldVNodeList = ref([])
const newVNodeList = ref([])
// 提示信息
const info = ref('')
The moving animation of the pointer can be realized by using the property css
of transition
the pointer element, as long as the value of the pointer element is modified , the moving animation of the left
real list can be easily realized by using the list transition component TransitionGroup , the template is as follows:DOM
Vue
<div class="playground">
<!-- 指针 -->
<div class="pointer">
<div
class="pointerItem"
v-for="item in oldPointerList"
:key="item.name"
:style="{ left: item.value * 120 + 'px' }"
>
<div class="pointerItemName">{
{ item.name }}</div>
<div class="pointerItemValue">{
{ item.value }}</div>
<img src="../assets/箭头_向下.svg" alt="" />
</div>
</div>
<div class="nodeListBox">
<!-- 旧节点列表 -->
<div class="nodeList">
<div class="name" v-if="oldVNodeList.length > 0">旧的VNode列表</div>
<div class="nodes">
<TransitionGroup name="list">
<div
class="nodeWrap"
v-for="(item, index) in oldVNodeList"
:key="item ? item.data.key : index"
>
<div class="node">{
{ item ? item.children : '空' }}</div>
</div>
</TransitionGroup>
</div>
</div>
<!-- 新节点列表 -->
<div class="nodeList">
<div class="name" v-if="newVNodeList.length > 0">新的VNode列表</div>
<div class="nodes">
<TransitionGroup name="list">
<div
class="nodeWrap"
v-for="(item, index) in newVNodeList"
:key="item.data.key"
>
<div class="node">{
{ item.children }}</div>
</div>
</TransitionGroup>
</div>
</div>
<!-- 提示信息 -->
<div class="info">{
{ info }}</div>
</div>
<!-- 指针 -->
<div class="pointer">
<div
class="pointerItem"
v-for="item in newPointerList"
:key="item.name"
:style="{ left: item.value * 120 + 'px' }"
>
<img src="../assets/箭头_向上.svg" alt="" />
<div class="pointerItemValue">{
{ item.value }}</div>
<div class="pointerItemName">{
{ item.name }}</div>
</div>
</div>
<!-- 真实DOM列表 -->
<div class="nodeList act" v-if="actNodeList.length > 0">
<div class="name">真实DOM列表</div>
<div class="nodes">
<TransitionGroup name="list">
<div
class="nodeWrap"
v-for="item in actNodeList"
:key="item.data.key"
>
<div class="node">{
{ item.children }}</div>
</div>
</TransitionGroup>
</div>
</div>
</div>
diff
The new list will not be modified during the double-ended algorithm VNode
, but the old VNode
list may be modified, that is, when the end-to-end comparison does not find a node that can be reused, but it is VNode
found by directly searching in the old list, Then the VNode
corresponding reality will be moved DOM
. After moving, it VNode
is actually equivalent to having been processed, but the VNode
position of the object is in the middle of the current pointer and cannot be deleted directly, so it has to be set to empty null
, so you can see in the template There are handles for this situation.
In addition, we also created an info
element to display the text information of the prompt as a description of the animation.
But this is not enough, because each old one VNode
has a corresponding real DOM
element, but what we input is just an ordinary json
data, so the template also needs to add a new list as the associated node of the old VNode
list. This list only needs to provide nodes It can be referenced and does not need to be visible, so set it display
to none
:
// 根据输入的旧VNode列表创建元素
const _oldVNodeList = computed(() => {
return JSON.parse(store.oldVNode)
})
// 引用DOM元素
const oldNode = ref(null)
const oldNodeList = ref([])
<!-- 隐藏 -->
<div class="hide">
<div class="nodes" ref="oldNode">
<div
v-for="(item, index) in _oldVNodeList"
:key="index"
ref="oldNodeList"
>
{
{ item.children }}
</div>
</div>
</div>
Then when we click the start button, we can assign values to our three list variables, and use h
functions to create new and old VNode
objects, and then pass them to the patched patch
function to start comparing and updating the actual DOM
elements:
const start = () => {
nextTick(() => {
// 表示当前真实的DOM列表
actNodeList.value = JSON.parse(store.oldVNode)
// 表示旧的VNode列表
oldVNodeList.value = JSON.parse(store.oldVNode)
// 表示新的VNode列表
newVNodeList.value = JSON.parse(store.newVNode)
nextTick(() => {
let oldVNode = h(
'div',
{
key: 1 },
JSON.parse(store.oldVNode).map((item, index) => {
// 创建VNode对象
let vnode = h(item.tag, item.data, item.children)
// 关联真实的DOM元素
vnode.el = oldNodeList.value[index]
return vnode
})
)
// 列表的父节点也需要关联真实的DOM元素
oldVNode.el = oldNode.value
let newVNode = h(
'div',
{
key: 1 },
JSON.parse(store.newVNode).map(item => {
return h(item.tag, item.data, item.children)
})
)
// 调用patch函数进行打补丁
patch(oldVNode, newVNode)
})
})
}
It can be seen that the new and old lists we input VNode
are used as child nodes of a node. This is because only when the two compared nodes have child nodes that are not text nodes, algorithms need to be used to diff
update their child nodes efficiently. When patch
After the function runs, you can open the console to view the hidden list, and you will find that it is consistent with the DOM
new list. Then you may ask, why not just use this list as the real list, and create an additional list yourself , in fact, it is possible, but in the process of the algorithm , the real nodes are moved by other methods , so it is not easy to add transition animations, only to see the nodes change positions instantly, which does not meet our animation needs.VNode
DOM
actNodeList
diff
insertBefore
DOM
The effect here is as follows:
Next, let's get out the pointer first. We create a processing function object, which will mount some methods for calling during the diff
algorithm and updating the corresponding variables in the function.
const handles = {
// 更新指针
updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx) {
oldPointerList.value = [
{
name: 'oldStartIdx',
value: oldStartIdx
},
{
name: 'oldEndIdx',
value: oldEndIdx
}
]
newPointerList.value = [
{
name: 'newStartIdx',
value: newStartIdx
},
{
name: 'newEndIdx',
value: newEndIdx
}
]
},
}
Then we can update the pointer in diff
the function handles.updatePointers()
:
const diff = (el, oldChildren, newChildren) => {
// 指针
// ...
handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
// ...
}
So the pointer comes out:
Then while
these four pointers will be constantly changed in the loop, so they also need to be updated in the loop:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...
handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
}
But this is obviously not possible, why, because the loop ends in an instant, and we hope to stay for a while every time, it is very simple, we write a waiting function:
const wait = t => {
return new Promise(resolve => {
setTimeout(
() => {
resolve()
},
t || 3000
)
})
}
Then we async/await
can easily implement waiting in loops using the syntax:
const diff = async (el, oldChildren, newChildren) => {
// ...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// ...
handles.updatePointers(oldStartIdx, oldEndIdx, newStartIdx, newEndIdx)
await wait()
}
}
Next we add two variables to highlight the two we are currently comparing VNode
:
// 当前比较中的节点索引
const currentCompareOldNodeIndex = ref(-1)
const currentCompareNewNodeIndex = ref(-1)
const handles = {
// 更新当前比较节点
updateCompareNodes(a, b) {
currentCompareOldNodeIndex.value = a
currentCompareNewNodeIndex.value = b
}
}
<div
class="nodeWrap"
v-for="(item, index) in oldVNodeList"
:key="item ? item.data.key : index"
:class="{
current: currentCompareOldNodeIndex === index,
}"
>
<div class="node">{
{ item ? item.children : '空' }}</div>
</div>
<div
class="nodeWrap"
v-for="(item, index) in newVNodeList"
:key="item.data.key"
:class="{
current: currentCompareNewNodeIndex === index,
}"
>
<div class="node">{
{ item.children }}</div>
</div>
Add a class name to the node in the current comparison for highlighting, the next step is still the same, you need to diff
call the function in the function, but how to add it:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if // ...
} else if (isSameNode(oldStartVNode, newStartVNode)) {
// ...
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldStartVNode, newEndVNode)) {
// ...
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
} else if (isSameNode(oldEndVNode, newStartVNode)) {
// ...
oldEndVNode = oldChildren[--oldEndIdx]
newStartVNode = newChildren[++newStartIdx]
} else if (isSameNode(oldEndVNode, newEndVNode)) {
// ...
oldEndVNode = oldChildren[--oldEndIdx]
newEndVNode = newChildren[--newEndIdx]
} else {
// ...
newStartVNode = newChildren[++newStartIdx]
}
We want to show the process of head-to-tail comparison, in fact, in these if
conditions, that is, to if
stay in each condition for a period of time, so can we do this directly:
const isSameNode = async () => {
// ...
handles.updateCompareNodes()
await wait()
}
if (await isSameNode(oldStartVNode, newStartVNode))
Unfortunately, I failed to try, so I can only rewrite it in other forms:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
let stop = false
let _isSameNode = false
if (oldStartVNode === null) {
callbacks.updateInfo('')
oldStartVNode = oldChildren[++oldStartIdx]
stop = true
}
// ...
if (!stop) {
callbacks.updateInfo('头-头比较')
callbacks.updateCompareNodes(oldStartIdx, newStartIdx)
_isSameNode = isSameNode(oldStartVNode, newStartVNode)
if (_isSameNode) {
callbacks.updateInfo(
'key值相同,可以复用,进行patch打补丁操作。新旧节点位置相同,不需要移动对应的真实DOM节点'
)
}
await wait()
}
if (!stop && _isSameNode) {
// ...
oldStartVNode = oldChildren[++oldStartIdx]
newStartVNode = newChildren[++newStartIdx]
stop = true
}
// ...
}
We use a variable to indicate whether we have entered a certain branch, and then save the result of checking whether the node can be reused in a variable, so that we can constantly check the values of these two variables to determine whether to enter the subsequent In the comparison branch, the logic of the comparison is not in if
the condition, and it can be used await
. At the same time, we also use updateInfo
the added prompt:
const handles = {
// 更新提示信息
updateInfo(tip) {
info.value = tip
}
}
Next, let’s look at the moving operation of the node. When the head ( oldStartIdx
corresponding oldStartVNode
node) and the tail ( newEndIdx
corresponding newEndVNode
node) are compared and found to be reusable, after the patch is completed, the oldStartVNode
corresponding real DOM
element needs to be moved to the position of oldEndVNode
the corresponding real DOM
element, that is Insert in front of oldEndVNode
the corresponding real DOM
back one node:
if (!stop && _isSameNode) {
// 头-尾
patchVNode(oldStartVNode, newEndVNode)
// 把oldStartVNode节点移动到最后
el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
// 更新指针
oldStartVNode = oldChildren[++oldStartIdx]
newEndVNode = newChildren[--newEndIdx]
stop = true
}
Then we can call the method of moving nodes of our simulated list immediately after the method insertBefore
moves the real elements:DOM
if (!stop && _isSameNode) {
// ...
el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)
callbacks.moveNode(oldStartIdx, oldEndIdx + 1)
// ...
}
What we want to operate is actually a list representing real DOM
nodes actNodeList
, so the key is to find out which one, first of all, the four node pointers at the head and tail indicate the position in the old and new VNode
lists, so we can obtain oldStartIdx
the corresponding Position , and then find the corresponding node in the list through the value , and perform operations such as moving, deleting, and inserting:oldEndIdx
oldVNodeList
VNode
key
actNodeList
const handles = {
// 移动节点
moveNode(oldIndex, newIndex) {
let oldVNode = oldVNodeList.value[oldIndex]
let newVNode = oldVNodeList.value[newIndex]
let fromIndex = findIndex(oldVNode)
let toIndex = findIndex(newVNode)
actNodeList.value[fromIndex] = '#'
actNodeList.value.splice(toIndex, 0, oldVNode)
actNodeList.value = actNodeList.value.filter(item => {
return item !== '#'
})
}
}
const findIndex = (vnode) => {
return !vnode
? -1
: actNodeList.value.findIndex(item => {
return item && item.data.key === vnode.data.key
})
}
Other insert nodes and delete nodes are similar:
Insert node:
const handles = {
// 插入节点
insertNode(newVNode, index, inNewVNode) {
let node = {
data: newVNode.data,
children: newVNode.text
}
let targetIndex = 0
if (index === -1) {
actNodeList.value.push(node)
} else {
if (inNewVNode) {
let vNode = newVNodeList.value[index]
targetIndex = findIndex(vNode)
} else {
let vNode = oldVNodeList.value[index]
targetIndex = findIndex(vNode)
}
actNodeList.value.splice(targetIndex, 0, node)
}
}
}
delete node:
const handles = {
// 删除节点
removeChild(index) {
let vNode = oldVNodeList.value[index]
let targetIndex = findIndex(vNode)
actNodeList.value.splice(targetIndex, 1)
}
}
The execution position of these methods in diff
the function is actually the place where the method is executed insertBefore
. removeChild
You can refer to the source code of this article for details, so I won’t introduce it here.
In addition, you can also highlight the elements that have been compared, the elements that will be added, the elements that will be deleted, etc., the final effect:
For time reasons, currently only the effect of the double-ended algorithm has been realized diff
, and the animation process of the fast algorithm will be added in the future diff
. If you are interested, you can pay attention~
Warehouse: https://github.com/wanglin2/VNode_visualization .