foreword
This article does not explain vDom implementation, mount mount, and render function. Only three diff algorithms are discussed. The VNode type does not consider component, functional-component, Fragment, and Teleport. Only Element and Text are considered. The entire code of this article can refer to this project.
Several methods will appear in the following diff algorithm, which are listed here and their functions are explained
-
mount(vnode, parent, [refNode])
: Byvnode
generating realDOM
nodes.parent
The real DOM node that is its parent,refNode
is the realDOM
node whose parent isparent
. IfrefNode
it is not empty,vnode
the generatedDOM
node will be insertedrefNode
before; ifrefNode
it is empty, thevnode
generatedDOM
node will be inserted as the last child nodeparent
in -
patch(prevNode, nextNode, parent)
: It can be simply understood asDOM
updating the current node, and callingdiff
the algorithm to compare its own child nodes;
1. React-Diff
The idea of React is incremental. By comparing the nodes in the new list, whether the position in the original list is increasing, to determine whether the current node needs to be moved.
1. Implementation principle
Let's look at such an example.
nextList
for the new list, prevList
for the old list. In this example, we can see at a glance that the new list does not need to be moved. Next, I use react
the incremental idea to explain why the nodes in the new list do not need to be moved.
We first traverse nextList
and find the position of each node in prevList
.
function foo(prevList, nextList) {
for (let i = 0; i < nextList.length; i++) {
let nextItem = nextList[i];
for (let j = 0; j < prevList.length; j++) {
let prevItem = prevList[j]
if (nextItem === prevItem) {
}
}
}
}
After finding the position, compare it with the position of the previous node. If the current position is greater than the previous position, it means that the current node does not need to move. So we need to define one lastIndex
to record the position of the previous node.
function foo(prevList, nextList) {
let lastIndex = 0
for (let i = 0; i < nextList.length; i++) {
let nextItem = nextList[i];
for (let j = 0; j < prevList.length; j++) {
let prevItem = prevList[j]
if (nextItem === prevItem) {
if (j < lastIndex) {
// 需要移动节点
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
}
}
}
}
In the above example, nextList
each node prevList
is at position 0 1 2 3
. Each item must be larger than the previous item, so no movement is required, which is the principle react
of diff
the algorithm.
2. Find the node that needs to be moved
In the previous section, we found the corresponding position by comparing whether the values are equal. But in vdom, each node is a vNode, how should we judge?
The answer is that key
we determine the uniqueness of each node by key
assigning values to each node and making the ones under the same array children
different , and compare the old and new lists.vnode
key
function reactDiff(prevChildren, nextChildren, parent) {
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i];
for (let j = 0; j < prevChildren.length; j++) {
let prevChild = prevChildren[j]
if (nextChild.key === prevChild.key) {
patch(prevChild, nextChild, parent)
if (j < lastIndex) {
// 需要移动节点
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
}
}
}
}
3. Mobile nodes
First of all, let's make it clear that the node referred to by the mobile node is DOM
a node. vnode.el
Points to the real node corresponding to this node DOM
. The method will assign the patch
updated node to the new property.DOM
vnode
el
For the convenience of drawing, we use
key
the value of to representvnode
the node. For the convenience of writing, we abbreviate thekey
value as , and the corresponding real DOM node isa
vnode
vnode-a
vnode-a
DOM-A
Let's substitute the example in the above figure into reactDiff
. We iterate over the new list , and find the position vnode
in the old list . When the traversal arrives vnode-d
, the position of the previous traversal in the old list is 0 < 2 < 3
, indicating that A C D
these three nodes do not need to be moved. At this point lastIndex = 3
, and enter the next cycle, it is found that vnode-b
in the old list is index
, 1
indicating 1 < 3
that DOM-B
it needs to be moved.
Through observation, we can find that we only need to DOM-B
move to DOM-D
the back. That is to find the VNode that needs to be moved. We call this VNode α, and move the real DOM node corresponding to α to the 新列表
back of the real DOM corresponding to the previous VNode in α.
In the above example, vnode-b
the corresponding real DOM node DOM-B
is moved to vnode-b
the previous one in the new list VNode
— behind the vnode-d
corresponding real DOM node .DOM-D
function reactDiff(prevChildren, nextChildren, parent) {
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i];
for (let j = 0; j < prevChildren.length; j++) {
let prevChild = prevChildren[j]
if (nextChild.key === prevChild.key) {
patch(prevChild, nextChild, parent)
if (j < lastIndex) {
// 移动到前一个节点的后面
let refNode = nextChildren[i - 1].el.nextSibling;
parent.insertBefore(nextChild.el, refNode)
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
}
}
}
}
Why is it moving like this? First, our list is 从头到尾
iterated. This means that for the current VNode
node, all the nodes before this node are sorted. If the node needs to be moved, you only need to move the DOM node after the previous vnode
node, because in the new listvnode
The sequence is like this.
4. Add nodes
In the previous section, we only talked about how to move nodes, but we ignored another situation, that is, there are brand new nodes in the new list , which cannot be found in the old list . In this case, we need to generate nodes based on new nodes and insert them into the tree.VNode
VNode
DOM
DOM
So far, we are faced with two problems: 1. How to discover new nodes, 2. DOM
Where to insert the generated nodes
Let's solve the first problem first. It is relatively simple to find nodes. We define a find
variable value as false
. If the same is found in the old list , change the value of . When the traversal is completed, the judgment value, if yes , indicates that the current node is a new node.key
vnode
find
true
find
false
function reactDiff(prevChildren, nextChildren, parent) {
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i],
find = false
for (let j = 0; j < prevChildren.length; j++) {
let prevChild = prevChildren[j]
if (nextChild.key === prevChild.key) {
find = true
patch(prevChild, nextChild, parent)
if (j < lastIndex) {
// 移动到前一个节点的后面
let refNode = nextChildren[i - 1].el.nextSibling
parent.insertBefore(nextChild.el, refNode)
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
break
}
}
if (!find) {
// 插入新节点
}
}
}
After finding a new node, the next step is where to insert it. The logic here is actually the same as that of moving nodes. We can see from the above figure that the new one vnode-c
is immediately vnode-b
behind, and vnode-b
the DOM nodes DOM-B
are already sorted, so we only need to vnode-c
insert the generated DOM nodes after DOM-B
that.
But here is a special case that needs attention, that is, the new node is located at the first of the new list . At this time, we need to find the first node of the old list , and insert the new node before the original first node.
function reactDiff(prevChildren, nextChildren, parent) {
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i],
find = false
for (let j = 0; j < prevChildren.length; j++) {
let prevChild = prevChildren[j]
if (nextChild.key === prevChild.key) {
find = true
patch(prevChild, nextChild, parent)
if (j < lastIndex) {
// 移动到前一个节点的后面
let refNode = nextChildren[i - 1].el.nextSibling
parent.insertBefore(nextChild.el, refNode)
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
break
}
}
if (!find) {
// 插入新节点
let refNode = i <= 0 ? prevChildren[0].el : nextChildren[i - 1].el.nextSibling
mount(nextChild, parent, refNode)
}
}
}
5. Remove the node
When there is an increase, there is a decrease. When the old node is not in the new list , we remove its corresponding DOM node.
function reactDiff(prevChildren, nextChildren, parent) {
let lastIndex = 0
for (let i = 0; i < nextChildren.length; i++) {
let nextChild = nextChildren[i],
find = false
for (let j = 0; j < prevChildren.length; j++) {
let prevChild = prevChildren[j]
if (nextChild.key === prevChild.key) {
find = true
patch(prevChild, nextChild, parent)
if (j < lastIndex) {
// 移动到前一个节点的后面
let refNode = nextChildren[i - 1].el.nextSibling
parent.insertBefore(nextChild.el, refNode)
} else {
// 不需要移动节点,记录当前位置,与之后的节点进行对比
lastIndex = j
}
break
}
}
if (!find) {
// 插入新节点
let refNode = i <= 0 ? prevChildren[0].el : nextChildren[i - 1].el.nextSibling
mount(nextChild, parent, refNode)
}
}
for (let i = 0; i < prevChildren.length; i++) {
let prevChild = prevChildren[i],
key = prevChild.key,
has = nextChildren.find((item) => item.key === key)
if (!has) parent.removeChild(prevChild.el)
}
}
6. Optimization and deficiencies
The above is the idea of React's diff algorithm.
The current reactDiff
time complexity is O(m*n)
, we can exchange space for time, and maintain the relationship key
with and index
as one Map
, so as to reduce the time complexity to O(n)
, the specific code can be viewed in this project .
Let's look at such an example next
According to reactDiff
the train of thought, we need to DOM-A
move to the back first DOM-C
, and then DOM-B
move to the back DOM-A
to complete Diff
. But we can find through observation that as long as we DOM-C
move to DOM-A
the front, it can be done Diff
.
There is room for optimization here. Next, we will introduce the algorithm vue2.x
in diff
-- 双端比较
, which solves the above problems
2. Vue2.X Diff - double-ended comparison
The so-called 双端比较
is that the head and tail of the new list and the old list are compared with each other. During the comparison process, the pointer will gradually move inward until all the nodes of a certain list have been traversed, and the comparison stops.
1. Implementation principle
We first use four pointers to point to the head and tail of the two lists
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1
;(newStartIndex = 0), (newEndIndex = nextChildren.length - 1)
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[nextStartIndex],
newEndNode = nextChildren[nextEndIndex]
}
We find four nodes according to the four pointers, and then compare them, so how to compare them? We follow the following four steps to compare
- Use the first node of the old list to compare
oldStartNode
with the first node of the new listnewStartNode
- Use the last node of the old list to compare
oldEndNode
with the last node of the new listnewEndNode
- Use the first node of the old list to compare
oldStartNode
with the last node of the new listnewEndNode
- Compare the last node of the old list
oldEndNode
with the first node of the new listnewStartNode
Use the above four steps for comparison to find key
the same reusable node, and stop the subsequent search when it is found in a certain step. The specific comparison sequence is as follows
The comparative sequence code structure is as follows:
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1
;(newStartIndex = 0), (newEndIndex = nextChildren.length - 1)
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newEndIndex]
if (oldStartNode.key === newStartNode.key) {
} else if (oldEndNode.key === newEndNode.key) {
} else if (oldStartNode.key === newEndNode.key) {
} else if (oldEndNode.key === newStartNode.key) {
}
}
When a reusable node is found during the comparison, we still patch the element patch
first , and then make the pointer 前/后移
a one-bit pointer. Depending on the comparison node, the pointer and direction we move are also different. The specific rules are as follows:
- Same when the first node of the old list is compared
oldStartNode
to the first node of the new list . Then the head pointer of the old list and the head pointer of the new list move backward one bit at the same time .newStartNode
key
oldStartIndex
newStartIndex
- Same when the last node of the old list is compared
oldEndNode
to the last node of the new list . Then the tail pointer of the old list and the tail pointer of the new list move forward one bit at the same time.newEndNode
key
oldEndIndex
newEndIndex
- Same when the first node of the old list is compared
oldStartNode
to the last node of the new list . Then the head pointer of the old list moves backward one bit; the tail pointer of the new list moves forward one bit.newEndNode
key
oldStartIndex
newEndIndex
- Same when comparing the last node of the old list
oldEndNode
with the first node of the new list . Then the tail pointer of the old list moves forward one bit; the head pointer of the new list moves back one bit.newStartNode
key
oldEndIndex
newStartIndex
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1,
newStartIndex = 0,
newEndIndex = nextChildren.length - 1
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newEndIndex]
if (oldStartNode.key === newStartNode.key) {
patch(oldvStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newEndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
oldEndIndex--
nextStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
At the beginning of the section, it was mentioned that the pointer should move inwards, so we need a loop. The condition for the loop to stop is when all the nodes in one of the lists have been traversed, the code is as follows
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1,
newStartIndex = 0,
newEndIndex = nextChildren.length - 1
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newEndIndex]
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldvStartNode, newEndNode, parent)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
}
So far we have completed the overall cycle. Next, we need to consider the following two issues:
- Under what circumstances
DOM
do nodes need to be moved DOM
How Nodes Move
Let's solve the first question: when it needs to be moved , let's take the above picture as an example.
When we were in the first loop, we 第四步
found that the tail node of the old listoldEndNode
is the same as the head node of the new listnewStartNode
, key
which is a reusable DOM
node. Through observation, we can find that the node that was originally at the end of the old list is the beginning node in the new list. No one is ahead of him, because he is the first, so we only need to move the current node to the original old list. Before the first node in the list, let it be the first node .
function vue2Diff(prevChildren, nextChildren, parent) {
// ...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
// ...
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
// 移动到旧列表头节点之前
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
}
Then we enter the second loop, and we 第二步
find that the tail node of the old listoldEndNode
and the tail node of the new listnewEndNode
are reused nodes. Originally, it was the tail node in the old list, and it is also the tail node in the new list, indicating that the node does not need to be moved , so we don't need to do anything.
Similarly, if the head node of the old listoldStartNode
and the head node of the new listnewStartNode
are multiplexing nodes, we don't need to do anything.
Entering the third cycle, we 第三部
find that the head node of the old listoldStartNode
and the tail node of the new listnewEndNode
are multiplexing nodes. At this point, if you are as smart as you are, you can see it at a glance, we just need to DOM-A
move it to the back.DOM-B
According to the convention, we still explain that the old list is the head node, and then the new list is the tail node. Then just move the current node behind the original tail node in the old list, and that's it .
function vue2Diff(prevChildren, nextChildren, parent) {
// ...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
//...
}
}
}
OK, enter the last cycle. The head node of 第一步
the old listoldStartNode
is at the same position as the head node of the new listnewStartNode
, so there is nothing to do. Then end the loop, that's Vue2 双端比较
how it works.
2. Non-ideal situations
In the last section, we talked about 双端比较
the principle, but there is a special case, when no reuse node is found in the four comparisons, we can only take the first node in the new list to find the same node in the old list .key
。
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex((child) => child.key === newKey)
}
}
}
When looking for a node, there are actually two situations: one is found in the old list , and the other is not found. Let's first take the above picture as an example and talk about the situation we found.
When we find the corresponding one in the old list VNode
, we only need to move the elements of the found node DOM
to the beginning. The logic here 第四步
is actually the same as that of , except that 第四步
it is the tail node of the move, and here is the node found by the move. DOM
After the move, we change the nodes in the old listundefined
. This is a crucial step, because we have already moved the nodes, so we don't need to compare again. Finally we newStartIndex
move the head pointer back one bit.
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newtKey = newStartNode.key,
oldIndex = prevChildren.findIndex((child) => child.key === newKey)
if (oldIndex > -1) {
let oldNode = prevChildren[oldIndex]
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
newStartNode = nextChildren[++newStartIndex]
}
}
}
What if the reuse node is not found in the old list ? It's very simple, just create a new node and put it at the front, and then move the head pointer backwards newStartIndex
.
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newtKey = newStartNode.key,
oldIndex = prevChildren.findIndex((child) => child.key === newKey)
if (oldIndex > -1) {
let oldNode = prevChildren[oldIndex]
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
} else {
mount(newStartNode, parent, oldStartNode.el)
}
newStartNode = nextChildren[++newStartIndex]
}
}
}
Finally, when the old list is traversed, undefind
the current node is skipped.
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode === undefind) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefind) {
oldEndNode = prevChildren[--oldEndIndex]
} else if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// ...
}
}
}
3. Add nodes
Let's look at an example first
This example is very simple. The tail node is the same for several cycles, and the tail pointer moves forward until the end of the cycle, as shown in the figure below
At this time oldEndIndex
and is less than oldStartIndex
, but there are remaining nodes in the new list , we only need to insert the remaining nodes before the in oldStartNode
turn DOM
. oldStartNode
Why is it before insertion ? The reason is that the position of the remaining nodes in the new list is located oldStartNode
before. If the remaining nodes are behind oldStartNode
, oldStartNode
it will be compared first. This needs to be thought about, but it is actually the 第四步
same as the idea.
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// ...
}
if (oldEndIndex < oldStartIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
}
}
4. Remove the node
Contrary to the situation in the previous section, when the new list is newEndIndex
less than newStartIndex
, we just delete the remaining nodes of the old list . Here we need to pay attention to the old listundefind
. As we mentioned in the second section, when the head and tail nodes are different, we will go to the old list to find the first node of the new list , and after moving the DOM node, change the node of the old listundefind
. So we need to pay attention to these when we delete at the end undefind
. If we encounter it, we can just skip the current cycle.
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// ...
}
if (oldEndIndex < oldStartIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
mount(nextChildren[i], parent, prevStartNode.el)
}
} else if (newEndIndex < newStartIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (prevChildren[i]) {
partent.removeChild(prevChildren[i].el)
}
}
}
}
5. Summary
This is 双端比较
all done, the following is the entire code.
function vue2diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
newStartIndex = 0,
oldStartIndex = prevChildren.length - 1,
newStartIndex = nextChildren.length - 1,
oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldStartIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newStartIndex]
while (oldStartIndex <= oldStartIndex && newStartIndex <= newStartIndex) {
if (oldStartNode === undefined) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefined) {
oldEndNode = prevChildren[--oldStartIndex]
} else if (oldStartNode.key === newStartNode.key) {
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldStartIndex--
newStartIndex--
oldEndNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newStartIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newStartIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldStartIndex--
newStartIndex++
oldEndNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else {
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex((child) => child && child.key === newKey)
if (oldIndex === -1) {
mount(newStartNode, parent, oldStartNode.el)
} else {
let prevNode = prevChildren[oldIndex]
patch(prevNode, newStartNode, parent)
parent.insertBefore(prevNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
newStartIndex++
newStartNode = nextChildren[newStartIndex]
}
}
if (newStartIndex > newStartIndex) {
while (oldStartIndex <= oldStartIndex) {
if (!prevChildren[oldStartIndex]) {
oldStartIndex++
continue
}
parent.removeChild(prevChildren[oldStartIndex++].el)
}
} else if (oldStartIndex > oldStartIndex) {
while (newStartIndex <= newStartIndex) {
mount(nextChildren[newStartIndex++], parent, oldStartNode.el)
}
}
}
3. Vue3 Diff - the longest increasing subsequence
vue3
Drawing diff
on inferno , the algorithm has two ideas. The first is the preprocessing of the same pre- and post-elements; the second is the longest increasing subsequence. This idea is React
similar diff
to but not the same as. Let's introduce them one by one.
1. Pre-processing and post-processing
Let's look at these two paragraphs
Hello World
Hey World
In fact, we can find out with a simple glance that some of the two paragraphs of text are the same. These texts do not need to be modified or moved . It is really necessary to modify the middle letters, so diff
it becomes the following part
text1: 'llo'
text2: 'y'
Next vnode
, let's take the following figure as an example.
The nodes framed in green in the figure do not need to be moved, they only need to be patched patch
. We write this logic as code.
function vue3Diff(prevChildren, nextChildren, parent) {
let j = 0,
prevEnd = prevChildren.length - 1,
nextEnd = nextChildren.length - 1,
prevNode = prevChildren[j],
nextNode = nextChildren[j]
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
j++
prevNode = prevChildren[j]
nextNode = nextChildren[j]
}
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
prevEnd--
nextEnd--
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
}
}
At this time, we need to consider the boundary conditions. There are two situations here. One is j > prevEnd
; the other is j > nextEnd
.
Let's take this picture as an example. j > prevEnd
At this time j <= nextEnd
, we only need to insert the remaining nodes between the new listj
and the new list . On the contrary, if yes, we can delete the nodes between and in the old list .nextEnd
j > nextEnd
j
prevEnd
function vue3Diff(prevChildren, nextChildren, parent) {
// ...
if (j > prevEnd && j <= nextEnd) {
let nextpos = nextEnd + 1,
refNode = nextpos >= nextChildren.length ? null : nextChildren[nextpos].el
while (j <= nextEnd) mount(nextChildren[j++], parent, refNode)
} else if (j > nextEnd && j <= prevEnd) {
while (j <= prevEnd) parent.removeChild(prevChildren[j++].el)
}
}
Let's continue to think about it. When we while
loop, the pointer gradually moves closer from both ends inward, so we should judge the boundary conditions in the loop. We use label
syntax. When we trigger the boundary conditions, we exit all the loops. Go straight to judgment. code show as below:
function vue3Diff(prevChildren, nextChildren, parent) {
let j = 0,
prevEnd = prevChildren.length - 1,
nextEnd = nextChildren.length - 1,
prevNode = prevChildren[j],
nextNode = nextChildren[j]
// label语法
outer: {
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
j++
// 循环中如果触发边界情况,直接break,执行outer之后的判断
if (j > prevEnd || j > nextEnd) break outer
prevNode = prevChildren[j]
nextNode = nextChildren[j]
}
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
prevEnd--
nextEnd--
// 循环中如果触发边界情况,直接break,执行outer之后的判断
if (j > prevEnd || j > nextEnd) break outer
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
}
}
// 边界情况的判断
if (j > prevEnd && j <= nextEnd) {
let nextpos = nextEnd + 1,
refNode = nextpos >= nextChildren.length ? null : nextChildren[nextpos].el
while (j <= nextEnd) mount(nextChildren[j++], parent, refNode)
} else if (j > nextEnd && j <= prevEnd) {
while (j <= prevEnd) parent.removeChild(prevChildren[j++].el)
}
}
2. Determine whether to move
In fact, looking at several algorithms, the routine is already obvious, which is to find the moving node and then move it to the correct position. Add the new nodes that should be added, delete the old nodes that should be deleted, and the whole algorithm is over. This algorithm is no exception, let's see how it works next.
When 前/后置
the preprocessing is over, we enter the real diff
link. First, we create an array based on the number of remaining nodes in the new listsource
, and fill the array -1
.
Let's write this logic first.
function vue3Diff(prevChildren, nextChildren, parent) {
//...
outer: {
// ...
}
// 边界情况的判断
if (j > prevEnd && j <= nextEnd) {
// ...
} else if (j > nextEnd && j <= prevEnd) {
// ...
} else {
let prevStart = j,
nextStart = j,
nextLeft = nextEnd - nextStart + 1, // 新列表中剩余的节点长度
source = new Array(nextLeft).fill(-1) // 创建数组,填满-1
}
}
So source
what is this array for? He is here to do the correspondence between the old and new nodes. We store the position of the new node in the old list in the array, and we use source
it 最长递增子序列
to move the DOM node according to the calculation. To this end, we first create an object to store the AND relationship in the current new list , and then go to the old list to find the position.节点
index
Pay attention when looking for nodes. If the old node is not in the new list, just delete it directly . In addition, we also need a number to indicate the patch
number of nodes we have passed. If the number is the same as the number of nodes remaining in the new list旧节点
, then we can delete the rest directly.
function vue3Diff(prevChildren, nextChildren, parent) {
//...
outer: {
// ...
}
// 边界情况的判断
if (j > prevEnd && j <= nextEnd) {
// ...
} else if (j > nextEnd && j <= prevEnd) {
// ...
} else {
let prevStart = j,
nextStart = j,
nextLeft = nextEnd - nextStart + 1, // 新列表中剩余的节点长度
source = new Array(nextLeft).fill(-1), // 创建数组,填满-1
nextIndexMap = {
}, // 新列表节点与index的映射
patched = 0 // 已更新过的节点的数量
// 保存映射关系
for (let i = nextStart; i <= nextEnd; i++) {
let key = nextChildren[i].key
nextIndexMap[key] = i
}
// 去旧列表找位置
for (let i = prevStart; i <= prevEnd; i++) {
let prevNode = prevChildren[i],
prevKey = prevNode.key,
nextIndex = nextIndexMap[prevKey]
// 新列表中没有该节点 或者 已经更新了全部的新节点,直接删除旧节点
if (nextIndex === undefind || patched >= nextLeft) {
parent.removeChild(prevNode.el)
continue
}
// 找到对应的节点
let nextNode = nextChildren[nextIndex]
patch(prevNode, nextNode, parent)
// 给source赋值
source[nextIndex - nextStart] = i
patched++
}
}
}
After finding the position, we observe the reassignment source
, we can see that if it is a brand new node, its source
corresponding value in the array is the initial value -1
, through this step we can distinguish which is a brand new node and which is Reusable.
Second, we have to judge whether we need to move. So how to judge the movement? It's very simple, React
we use the incremental method as in the same method, if what we find index
is always increasing, it means that there is no need to move any nodes. We set a variable to save the state of whether we need to move.
function vue3Diff(prevChildren, nextChildren, parent) {
//...
outer: {
// ...
}
// 边界情况的判断
if (j > prevEnd && j <= nextEnd) {
// ...
} else if (j > nextEnd && j <= prevEnd) {
// ...
} else {
let prevStart = j,
nextStart = j,
nextLeft = nextEnd - nextStart + 1, // 新列表中剩余的节点长度
source = new Array(nextLeft).fill(-1), // 创建数组,填满-1
nextIndexMap = {
}, // 新列表节点与index的映射
patched = 0,
move = false, // 是否移动
lastIndex = 0 // 记录上一次的位置
// 保存映射关系
for (let i = nextStart; i <= nextEnd; i++) {
let key = nextChildren[i].key
nextIndexMap[key] = i
}
// 去旧列表找位置
for (let i = prevStart; i <= prevEnd; i++) {
let prevNode = prevChildren[i],
prevKey = prevNode.key,
nextIndex = nextIndexMap[prevKey]
// 新列表中没有该节点 或者 已经更新了全部的新节点,直接删除旧节点
if (nextIndex === undefind || patched >= nextLeft) {
parent.removeChild(prevNode.el)
continue
}
// 找到对应的节点
let nextNode = nextChildren[nextIndex]
patch(prevNode, nextNode, parent)
// 给source赋值
source[nextIndex - nextStart] = i
patched++
// 递增方法,判断是否需要移动
if (nextIndex < lastIndex) {
move = false
} else {
lastIndex = nextIndex
}
}
if (move) {
// 需要移动
} else {
//不需要移动
}
}
}
3. How the DOM moves
After judging whether we need to move, we need to consider how to move. Once a DOM move is required, the first thing we need to do is find source
the longest increasing subsequence .
function vue3Diff(prevChildren, nextChildren, parent) {
//...
if (move) {
const seq = lis(source) // [0, 1]
// 需要移动
} else {
//不需要移动
}
}
What is the longest increasing subsequence: Given a numerical sequence, find a subsequence of it, and the value in the subsequence is increasing, and the elements in the subsequence are not necessarily continuous in the original sequence.
For example, the given numerical sequence is: [ 0, 8, 4, 12 ].
Then its longest increasing subsequence is: [0, 8, 12].
Of course, the answer may have many situations, for example: [0, 4, 12] is also possible.
We explain the longest increasing subsequence separately in the next section
In the above code, we call the function to find the longest increasing subsequence of lis
the array . We know the value of the source array , obviously the longest increasing subsequence should be , but why is the calculated result ? In fact, it represents the position index of each element in the longest increasing subsequence in the array, as shown in the following figure:source
[ 0, 1 ]
[2, 3, 1, -1]
[ 2, 3 ]
[ 0, 1 ]
[ 0, 1 ]
source
We renumbered the new listsource
according to , and found out .最长递增子序列
We iterate through source
each item from back to front. At this point, three situations will occur:
- The current value
-1
means that the node is a brand new node, and since we are traversing from the back to the front , we can directly create a DOM node and insert it at the end of the queue. - The current index
最长递增子序列
is the value in , that isi === seq[j]
, this means that the node does not need to be moved - If the current index is not
最长递增子序列
the value in , it means that the DOM node needs to be moved. It is also easy to understand here. We can also directly insert the DOM node into the end of the queue, because the end of the queue is sorted.
function vue3Diff(prevChildren, nextChildren, parent) {
//...
if (move) {
// 需要移动
const seq = lis(source); // [0, 1]
let j = seq.length - 1; // 最长子序列的指针
// 从后向前遍历
for (let i = nextLeft - 1; i >= 0; i--) {
let pos = nextStart + i, // 对应新列表的index
nextNode = nextChildren[pos], // 找到vnode
nextPos = pos + 1, // 下一个节点的位置,用于移动DOM
refNode = nextPos >= nextChildren.length ? null : nextChildren[nextPos].el, //DOM节点
cur = source[i]; // 当前source的值,用来判断节点是否需要移动
if (cur === -1) {
// 情况1,该节点是全新节点
mount(nextNode, parent, refNode)
} else if (cur === seq[j]) {
// 情况2,是递增子序列,该节点不需要移动
// 让j指向下一个
j--
} else {
// 情况3,不是递增子序列,该节点需要移动
parent.insetBefore(nextNode.el, refNode)
}
}
} else {
//不需要移动
}
}
After talking about the situation that needs to be moved, let's talk about the situation that does not need to move. If there is no need to move, we only need to judge whether there is a new node to add to him. The specific code is as follows:
function vue3Diff(prevChildren, nextChildren, parent) {
//...
if (move) {
const seq = lis(source); // [0, 1]
let j = seq.length - 1; // 最长子序列的指针
// 从后向前遍历
for (let i = nextLeft - 1; i >= 0; i--) {
let pos = nextStart + i, // 对应新列表的index
nextNode = nextChildren[pos], // 找到vnode
nextPos = pos + 1, // 下一个节点的位置,用于移动DOM
refNode = nextPos >= nextChildren.length ? null : nextChildren[nextPos].el, //DOM节点
cur = source[i]; // 当前source的值,用来判断节点是否需要移动
if (cur === -1) {
// 情况1,该节点是全新节点
mount(nextNode, parent, refNode)
} else if (cur === seq[j]) {
// 情况2,是递增子序列,该节点不需要移动
// 让j指向下一个
j--
} else {
// 情况3,不是递增子序列,该节点需要移动
parent.insetBefore(nextNode.el, refNode)
}
}
} else {
//不需要移动
for (let i = nextLeft - 1; i >= 0; i--) {
let cur = source[i]; // 当前source的值,用来判断节点是否需要移动
if (cur === -1) {
let pos = nextStart + i, // 对应新列表的index
nextNode = nextChildren[pos], // 找到vnode
nextPos = pos + 1, // 下一个节点的位置,用于移动DOM
refNode = nextPos >= nextChildren.length ? null : nextChildren[nextPos].el, //DOM节点
mount(nextNode, parent, refNode)
}
}
}
}
So far vue3.0
the diff is complete.
4. Longest increasing subsequence
Let's take the array as an example
;[10, 9, 2, 5, 3, 8, 7, 13]
We can use the idea of dynamic programming to consider this problem. The idea of dynamic programming is to decompose a large problem into multiple small sub-problems, and try to get the optimal solution of these sub-problems. The optimal solution of the sub-problems may be used in larger problems, so through Optimal solutions to small problems eventually lead to optimal solutions to large problems.
We first assume that there is only one value array [13]
, then the longest increasing subsequence of the array is [13]
itself, and its length is 1
. Then we think that the length of the increasing sequence of each item is 1
Then we add a value to the array this time [7, 13]
, because 7 < 13
the longest increasing subsequence of the array is [7, 13]
, then the length is 2
. Then can we think that when it is [7]
less than ,[13]
the length of the incremental sequence of the header is the sum of the length of and the length of[7]
[7]
[13]
, ie 1 + 1 = 2
.
Ok, let's calculate the array based on this idea. We first assign the initial assignment of each value to1
First of 7 < 13
all, 7
the corresponding length is 13
the length of plus 1,1 + 1 = 2
Go ahead, let's compare 8
. We first 7
compare and find that the increment is not satisfied, but it does not matter that we can continue to 13
compare and 8 < 13
satisfy the increment, then 8
the length of the length is also 13
increased by one, and the length is2
Let's compare again 3
, let's 8
compare it with 3 < 8
, then 3
the length of is 8
the length of plus one, and 3
the length at this time is 3
. But it's not over yet, we still need to make 3
comparisons 7
. Similarly 3 < 7
, at this time we need to calculate a length that is the 7
length plus one 3
. We compare the two lengths. If the original length is not as large as the calculated length, we replace it. Otherwise, we keep the original value . Since 3 === 3
, we choose not to replace. Finally, let's compare 3
with and 13
, similarly 3 < 13
, the calculated length at this time is 2
, which is smaller than the original length 3
, and we choose to keep the original value.
The subsequent calculations are followed by analogy, and the final result is like this
We take the largest value from 4
it, which represents the number of longest increasing subsequences . code show as below:
function lis(arr) {
let len = arr.length,
dp = new Array(len).fill(1) // 用于保存长度
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i]
for (let j = i + 1; j < len; j++) {
let next = arr[j]
// 如果是递增 取更大的长度值
if (cur < next) dp[i] = Math.max(dp[j] + 1, dp[i])
}
}
return Math.max(...dp)
}
So far, we have covered the basic longest increasing subsequence. However, in vue3.0
, what we need is the index of the longest increasing subsequence in the original array. So we also need to create an array to save the longest subsequence of each value corresponding to the array index
. The specific code is as follows:
function lis(arr) {
let len = arr.length,
res = [],
dp = new Array(len).fill(1)
// 存默认index
for (let i = 0; i < len; i++) {
res.push([i])
}
for (let i = len - 1; i >= 0; i--) {
let cur = arr[i],
nextIndex = undefined
// 如果为-1 直接跳过,因为-1代表的是新节点,不需要进行排序
if (cur === -1) continue
for (let j = i + 1; j < len; j++) {
let next = arr[j]
// 满足递增条件
if (cur < next) {
let max = dp[j] + 1
// 当前长度是否比原本的长度要大
if (max > dp[i]) {
dp[i] = max
nextIndex = j
}
}
}
// 记录满足条件的值,对应在数组中的index
if (nextIndex !== undefined) res[i].push(...res[nextIndex])
}
let index = dp.reduce((prev, cur, i, arr) => (cur > arr[prev] ? i : prev), dp.length - 1)
// 返回最长的递增子序列的index
return result[index]
}
Author of this article: JD Technology Sun Yanzhe
For more best practices & innovations in digital technology, please pay attention to the WeChat public account of "JD Digital Technology Talk"