The diff algorithm of React, Vue2.x, Vue3.0

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]): By vnodegenerating real DOMnodes. parentThe real DOM node that is its parent, refNodeis the real DOMnode whose parent is parent. If refNodeit is not empty, vnodethe generated DOMnode will be inserted refNodebefore; if refNodeit is empty, the vnodegenerated DOMnode will be inserted as the last child node parentin

  • patch(prevNode, nextNode, parent): It can be simply understood as DOMupdating the current node, and calling diffthe 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.

nextListfor the new list, prevListfor 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 reactthe incremental idea to explain why the nodes in the new list do not need to be moved.

We first traverse nextListand 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 lastIndexto 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, nextListeach node prevListis at position 0 1 2 3. Each item must be larger than the previous item, so no movement is required, which is the principle reactof diffthe 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 keywe determine the uniqueness of each node by keyassigning values ​​to each node and making the ones under the same array childrendifferent , and compare the old and new lists.vnodekey

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 DOMa node. vnode.elPoints to the real node corresponding to this node DOM. The method will assign the patchupdated node to the new property.DOMvnodeel

For the convenience of drawing, we use keythe value of to represent vnodethe node. For the convenience of writing, we abbreviate the keyvalue as , and the corresponding real DOM node isavnodevnode-avnode-aDOM-A

Let's substitute the example in the above figure into reactDiff. We iterate over the new list , and find the position vnodein 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 Dthese three nodes do not need to be moved. At this point lastIndex = 3, and enter the next cycle, it is found that vnode-bin the old list is index, 1indicating 1 < 3that DOM-Bit needs to be moved.

Through observation, we can find that we only need to DOM-Bmove to DOM-Dthe 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-bthe corresponding real DOM node DOM-Bis moved to vnode-bthe previous one in the new list VNodebehind the vnode-dcorresponding 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 VNodenode, 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 vnodenode, 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.VNodeVNodeDOMDOM

So far, we are faced with two problems: 1. How to discover new nodes, 2. DOMWhere to insert the generated nodes

Let's solve the first problem first. It is relatively simple to find nodes. We define a findvariable 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
vnodefindtruefindfalse

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-cis immediately vnode-bbehind, and vnode-bthe DOM nodes DOM-Bare already sorted, so we only need to vnode-cinsert the generated DOM nodes after DOM-Bthat.

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 reactDifftime complexity is O(m*n), we can exchange space for time, and maintain the relationship keywith and indexas 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 reactDiffthe train of thought, we need to DOM-Amove to the back first DOM-C, and then DOM-Bmove to the back DOM-Ato complete Diff. But we can find through observation that as long as we DOM-Cmove to DOM-Athe front, it can be done Diff.

There is room for optimization here. Next, we will introduce the algorithm vue2.xin 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

  1. Use the first node of the old list to compare oldStartNodewith the first node of the new listnewStartNode
  2. Use the last node of the old list to compare oldEndNodewith the last node of the new listnewEndNode
  3. Use the first node of the old list to compare oldStartNodewith the last node of the new listnewEndNode
  4. Compare the last node of the old listoldEndNode with the first node of the new listnewStartNode

Use the above four steps for comparison to find keythe 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 patchfirst , 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:

  1. Same when the first node of the old list is compared oldStartNodeto 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 .newStartNodekeyoldStartIndexnewStartIndex
  2. Same when the last node of the old list is compared oldEndNodeto 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.newEndNodekeyoldEndIndexnewEndIndex
  3. Same when the first node of the old list is compared oldStartNodeto 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.newEndNodekeyoldStartIndexnewEndIndex
  4. Same when comparing the last node of the old listoldEndNode 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.newStartNodekeyoldEndIndexnewStartIndex
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 DOMdo nodes need to be moved
  • DOMHow 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 , keywhich is a reusable DOMnode. 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-Amove 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 DOMto 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. DOMAfter 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 newStartIndexmove 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, undefindthe 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 oldEndIndexand is less than oldStartIndex, but there are remaining nodes in the new list , we only need to insert the remaining nodes before the in oldStartNodeturn DOM. oldStartNodeWhy is it before insertion ? The reason is that the position of the remaining nodes in the new list is located oldStartNodebefore. If the remaining nodes are behind oldStartNode, oldStartNodeit 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 newEndIndexless 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

vue3Drawing diffon 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 Reactsimilar diffto 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 diffit 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 > prevEndAt 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 .nextEndj > nextEndjprevEnd

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 whileloop, the pointer gradually moves closer from both ends inward, so we should judge the boundary conditions in the loop. We use labelsyntax. 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 difflink. 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 sourcewhat 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 sourceit 最长递增子序列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 patchnumber 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 sourcecorresponding 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, Reactwe use the incremental method as in the same method, if what we find indexis 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 sourcethe 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 listhe 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 sourceeach item from back to front. At this point, three situations will occur:

  1. The current value -1means 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.
  2. The current index 最长递增子序列is the value in , that is i === seq[j], this means that the node does not need to be moved
  3. 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.0the diff is complete.

4. Longest increasing subsequence

Leetcode has the original question, and the official analysis is very clear. If you don’t understand what I’m talking about, you can go to the official analysis.

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 < 13the 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 < 13all, 7the corresponding length is 13the length of plus 1,1 + 1 = 2

Go ahead, let's compare 8. We first 7compare and find that the increment is not satisfied, but it does not matter that we can continue to 13compare and 8 < 13satisfy the increment, then 8the length of the length is also 13increased by one, and the length is2

Let's compare again 3, let's 8compare it with 3 < 8, then 3the length of is 8the length of plus one, and 3the length at this time is 3. But it's not over yet, we still need to make 3comparisons 7. Similarly 3 < 7, at this time we need to calculate a length that is the 7length 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 3with 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 4it, 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"
insert image description here

Guess you like

Origin blog.csdn.net/JDDTechTalk/article/details/112860897