1 Introduction
In the previous article, we introduced that when both the new VNode
and the old oldVNode
are element nodes and both contain child nodes, Vue
the child nodes are
First the outer loop newChildren
array, and then the inner loop oldChildren
array, every time newChildren
a child node in the outer array is looped, go to the inner oldChildren
array to see if there is a child node that is the same as it, and finally make different operations according to different situations.
At the end of the previous article, we also said that although this method can solve the problem, there are still places that can be optimized. For example, when the number of child nodes is large, the time complexity of the loop algorithm will become very large, which is not conducive to performance improvement. Of course, Vue
I also realized this and optimized it, so in this article, let’s learn Vue
how to optimize the update of child nodes.
2. Introduction to Optimization Strategies
Suppose we have a new newChildren
array and an old oldChildren
array, as follows:
newChildren = ['新子节点1','新子节点2','新子节点3','新子节点4']
oldChildren = ['旧子节点1','旧子节点2','旧子节点3','旧子节点4']
If we follow the solution before optimization, then our next operation should be like this: first loop through newChildren
the array, get the first new child node 1, and then use the first new child node 1 to follow the oldChildren
old child node in the array Comparing one by one, if luck is a little bit better, oldChildren
the first old child node 1 in the array happens to be the same as the first new child node 1, then everyone is happy, and they can be processed directly without further looping down. Then if the luck is a bit bad, until oldChildren
the fourth old child node 4 in the loop is the same as the first new child node 1, then it will loop 4 more times at this time. We might as well imagine the situation to be a little more extreme. If neither newChildren
the array nor oldChildren
the first three nodes in the array have changed, but only the fourth node has changed, then we will loop 16 times and only find out at the 16th loop The new node 4 is the same as the old node 4 and needs to be updated, as shown in the figure below:
In the above example, there are only four child nodes, and it seems that there are no defects. However, when the number of child nodes is large, the time complexity of the algorithm will be very high, which is not conducive to performance improvement.
So how can we optimize it? In fact, we can think of it this way, instead of looping through the two arrays in order newChildren
, oldChildren
we can first compare the child nodes at special positions in the two arrays, for example:
- First compare the
newChildren
first child nodes of all unprocessed child nodes in the array with the first child nodes of all unprocessed child nodes in the array, if they are the same, then directly enter the operation of updating nodes;oldChildren
- If they are different, compare
newChildren
the last child node of all unprocessed child nodes in the array with theoldChildren
last child node of all unprocessed child nodes in the array. If they are the same, then directly enter the operation of updating the node; - If they are different, compare the
newChildren
last child node of all unprocessed child nodes in the array with the first child node of all unprocessed child nodes in the array. If they are the same, then go directly to the update node operation. After the updateoldChildren
ThenoldChildren
move the node in the array tonewChildren
the same position as the node in the array; - If they are different, compare
newChildren
the first child node of all unprocessed child nodes inoldChildren
the array with the last child node of all unprocessed child nodes in the array. If they are the same, then go directly to the update node operation. After the update is complete ThenoldChildren
move the node in the array tonewChildren
the same position as the node in the array; - After trying the last four cases, if they are still different, then find the nodes according to the previous loop method.
The process is shown in the figure below:
In the image above, we put:
newChildren
The first child node of all unprocessed child nodes in the array is called: new before;newChildren
The last child node of all unprocessed child nodes in the array is called: new post;oldChildren
The first child node of all unprocessed child nodes in the array is called: old front;oldChildren
The last child node of all unprocessed child nodes in the array is called: old post;
OK, now that we have the above concepts, let's take a look at how it is implemented.
3. New Front vs. Old Front
Compare newChildren
the first child node of all unprocessed child nodes in the array with oldChildren
the first child node of all unprocessed child nodes in the array. If they are the same, that's great. Go directly to the update node mentioned in the previous article and since the positions of the new and old nodes are the same, there is no need to perform node movement operations; if they are different, it does not matter, and then try the next three cases.
4. The new post and the old post
Compare newChildren
the last child node of all unprocessed child nodes in the array with the oldChildren
last child node of all unprocessed child nodes in the array, if they are the same, then directly enter the operation of updating the node and because the new one and the old one The positions of the nodes are also the same, so there is no need to move the nodes; if they are different, continue to try later.
5. New after vs old front
Compare newChildren
the last child node of all unprocessed child nodes in the array with oldChildren
the first child node of all unprocessed child nodes in the array, if they are the same, then directly enter the operation of updating the node, and then update the oldChildren
array Move the node to newChildren
the same position as the node in the array;
At this time, the operation of moving the node appears. The most critical part of moving the node is to find the location to be moved. We have repeatedly emphasized that updating nodes should be VNode
based on the new ones, and then operate the old ones oldVNode
so that the old ones are the same oldVNode
as the new onesVNode
. Then the current situation is: newChildren
the last child node in the array oldChildren
is the same as the first child node in the array, then we should oldChildren
move the first child node to the position of the last child node in the array, as shown in the figure below:
It is not difficult to see from the figure that we need to oldChildren
move the first child node in the array after all unprocessed nodes in the array .
If after the comparison, it is found that the two nodes are still not the same node, then continue to try the last case.
6. New before and old after
Compare newChildren
the first child node of all unprocessed child nodes in the array with oldChildren
the last child node of all unprocessed child nodes in the array. If they are the same, then go directly to the operation of updating the node, and then update the node in the oldChildren
array Move the node to newChildren
the same position as the node in the array;
Similarly, the logic of node moving position in this case is similar to the logic of "new after and old before", that is, the newChildren
first child node in the array oldChildren
is the same as the last child node in the array, then we should oldChildren
put in the array The last child node moves to the position of the first child node, as shown in the figure below:
It is not difficult to see from the figure that we need to oldChildren
move the last child node in the array before all unprocessed nodes in the array .
OK, the above are the 4 cases of sub-node comparison and update optimization strategies. If the same node is not found after trying the above 4 cases one by one, then search through the previous loop method.
7. Go back to the source code
After analyzing the ideas and clarifying the logic, let's go back to the source code to see if the logic implemented by the source code is the same as what we analyzed. The source code is as follows:
// 循环更新子节点
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // oldChildren开始索引
let oldEndIdx = oldCh.length - 1 // oldChildren结束索引
let oldStartVnode = oldCh[0] // oldChildren中所有未处理节点中的第一个
let oldEndVnode = oldCh[oldEndIdx] // oldChildren中所有未处理节点中的最后一个
let newStartIdx = 0 // newChildren开始索引
let newEndIdx = newCh.length - 1 // newChildren结束索引
let newStartVnode = newCh[0] // newChildren中所有未处理节点中的第一个
let newEndVnode = newCh[newEndIdx] // newChildren中所有未处理节点中的最后一个
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 (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 如果oldStartVnode不存在,则直接跳过,比对下一个
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果新前与旧前节点相同,就把两个节点进行patch更新
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果新后与旧后节点相同,就把两个节点进行patch更新
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果不属于以上四种情况,就进行常规的循环比对patch
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果在oldChildren里找不到当前循环的newChildren里的子节点
if (isUndef(idxInOld)) {
// New element
// 新增节点并插入到合适位置
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 如果在oldChildren里找到了当前循环的newChildren里的子节点
vnodeToMove = oldCh[idxInOld]
// 如果两个节点相同
if (sameVnode(vnodeToMove, newStartVnode)) {
// 调用patchVnode更新节点
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
// canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
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) {
/**
* 如果oldChildren比newChildren先循环完毕,
* 那么newChildren里面剩余的节点都是需要新增的节点,
* 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/**
* 如果newChildren比oldChildren先循环完毕,
* 那么oldChildren里面剩余的节点都是需要删除的节点,
* 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Before reading the source code, we first have such a concept: that is, in the optimization strategy we mentioned earlier, nodes may be compared from the front or from the back, and the update process will be performed if the comparison is successful, that is to say, we It is possible to process the first one, and it is also possible to process the last one, so when we cycle, we cannot simply cycle from front to back or from back to front, but from both sides to the middle.
So how to loop from both sides to the middle? Please see the picture below:
First, we prepare 4 variables:
- newStartIdx:
newChildren
the subscript of the start position in the array; - newEndIdx:
newChildren
the subscript of the end position in the array; - oldStartIdx:
oldChildren
the subscript of the starting position in the array; - oldEndIdx:
oldChildren
the subscript of the end position in the array;
When looping, each time a node is processed, the subscript is moved to the direction indicated by the arrow in the figure. After the node represented by the start position is processed, it is moved backward by one position; the node represented by the end position is After processing, move forward one position; since our optimization strategy is to update the old and new nodes in pairs, one update will move two nodes. To put it more bluntly: newStartIdx
and oldStartIdx
can only move backward (only add), newEndIdx
and oldEndIdx
can only move forward (only subtract).
When the start position is greater than the end position, it means that all nodes have been traversed.
OK, with this concept, we start to read the source code:
-
If
oldStartVnode
it does not exist, skip it directly,oldStartIdx
add 1, and compare the next// 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] } }
-
If
oldEndVnode
it does not exist, it will be skipped directly, andoldEndIdx
1 will be subtracted, compared with the previous oneelse if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] }
-
If the new front node is the same as the old front node, update the two nodes
patch
, add 1 to the sum at the same time ,oldStartIdx
andnewStartIdx
move one position backelse if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }
-
If the new node is the same as the old node, update the two nodes
patch
, decrease the sum by 1,oldEndIdx
andnewEndIdx
move one position forwardelse if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] }
-
If the new node is the same as the old node, first update the two nodes
patch
, then move the old nodeoldChilren
behind all the unprocessed nodes, and finallyoldStartIdx
add 1, move back one position,newEndIdx
subtract 1, and move forward one positionelse if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }
-
If the new front node is the same as the old back node, first update the two nodes
patch
, then move the old back node tooldChilren
the front of all unprocessed nodes, and finally addnewStartIdx
1, move back one position,oldEndIdx
subtract 1, and move forward one positionelse if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }
-
If it does not belong to the above four situations, perform a regular cycle comparison
patch
-
If in the loop,
oldStartIdx
it is greateroldEndIdx
than, it means that the cycle is completedoldChildren
thannewChildren
before, thennewChildren
the remaining nodes in it are nodes that need to be added, and[newStartIdx, newEndIdx]
all the nodes in between are inserted intoDOM
theif (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) }
-
If in the cycle,
newStartIdx
it is greaternewEndIdx
than, it means that the cycle is completednewChildren
thanoldChildren
before, thenoldChildren
the remaining nodes in it are the nodes that need to be deleted, and[oldStartIdx, oldEndIdx]
all the nodes in between are deletedelse if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) }
OK, the processing is complete. It can be seen that the processing logic in the source code is the same as the logic we analyzed before.
8. Summary
In this article, we introduced Vue
the optimization strategy for neutron node update, and found that Vue
in order to avoid the performance problems caused by the large amount of data in the double loop and the increase in time complexity, we chose to compare the four special positions in the sub-node array with each other. Yes, they are: new front and old front, new rear and old rear, new rear and old front, new front and old rear. For each case, we analyzed its logic in the form of graphics and text. Finally, we go back to the source code and verify that our analysis is correct by reading the source code. Fortunately, we have found the corresponding implementation in the source code for each step of our previous analysis, which can verify that our analysis is correct. The above is the process Vue
in the middle patch
, that is, DOM-Diff
all the contents of the algorithm. I believe you will have a clearer idea when you read this part of the source code again.