Snabbdom introduction

Snabbdom is a virtual DOM library focused on providing a simple, modular experience with powerful functionality and performance. The diff in vue2. X borrows snabbDOM. So we take snabbDOM source code as the research object in this article. Snabbdom making links

  • Snabbdom is one of the fastest Virtual DOM libraries tested by the Virtual DOM Benchmark
  • It can be extended by modules

Since SNabbDOM was written in TypeScript prior to v0.6.0, we understand the SNabbDOM primarily in TypeScript. The TypeScript library is useful for reading source code. Although it looks like a lot more type judgment, it also makes the code more readable. For example, v-Nodes in the library have a lot of parameters, as shown below

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined) :VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
Copy the code

So we can easily get what each parameter in Vnode represents, so in the following content, we will also use Vnode as the entry point to understand the main function of snabbDOM

Virtual DOM, Virtual Node (Vnode)

Virtual DOM: Describes the DOM hierarchy with JavaScript objects. All properties in the DOM have corresponding properties in the virtual DOM

As for how to convert DOM into virtual DOM, you can refer to my last share, which is similar to the process of transforming DOM into tokens in this note. If you are interested, you can read it for yourself and I won’t introduce it any more. Mustache template engine link

In SNabbDOM, the author uses an H function to process our virtual DOM simply, and encapsulates our irregular virtual DOM into Vnode virtual nodes with corresponding positions of various parameters. Let’s take another look at Vnode in the source code

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined) :VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}
Copy the code

Let me briefly explain what each parameter in Vnode means

  1. Sel: Various tags in the DOM such as “div”,” P “,” H1 “,” A “, etc
  2. Data: Attributes and styles on the tag, such as links in the A tag, classes, etc
  3. Children: The child node in the DOM
  4. Text: indicates the text content
  5. Elm: Mounted label
  6. Key: Serving minimum Updates (DIFF) will be covered later

H function

So what does h do, let’s look at the core of h first

export function h (sel: string) :VNode
export function h (sel: string, data: VNodeData | null) :VNode
export function h (sel: string, children: VNodeChildren) :VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren) :VNode
export function h (sel: any, b? :any, c? :any) :VNode {
 var data: VNodeData = {}
 var children: any
 var text: any
 var i: number
 if (c ! = =undefined) {
   if (b ! = =null) {
     data = b
   }
   if (is.array(c)) {
     children = c
   } else if (is.primitive(c)) {
     text = c
   } else if (c && c.sel) {
     children = [c]
   }
 } else if(b ! = =undefined&& b ! = =null) {
   if (is.array(b)) {
     children = b
   } else if (is.primitive(b)) {
     text = b
   } else if (b && b.sel) {
     children = [b]
   } else { data = b }
 }
 if(children ! = =undefined) {
   for (i = 0; i < children.length; ++i) {
     if (is.primitive(children[i])) children[i] = vnode(undefined.undefined.undefined, children[i], undefined)}}if (
   sel[0= = ='s' && sel[1= = ='v' && sel[2= = ='g' &&
   (sel.length === 3 || sel[3= = ='. ' || sel[3= = =The '#')
 ) {
   addNS(data, children, sel)
 }
 return vnode(sel, data, children, text, undefined)};Copy the code

In the h function, we can see that we make all sorts of if judgments about a, B, and C, which are essentially judgments about what parameters are missing from the virtual DOM. Makes the parameter passed to the corresponding position.

Here I have introduced the H function from the SNabbDOM to convert the following virtual DOM into a Vnode for understanding Once we have the Vnode we want, there are only two things we need to do, regardless of the other side features (style recognition, class recognition, etc.)

  1. Using the DIff algorithm. Let us know the difference between oldVnode (the old node) and newVnode (the new node).
  2. Once you know the difference, update the old node to the new node at the minimum, rendering the different virtual DOM into the real DOM.

The diff algorithm

Introduction of diff

Here we take a look at diff, the new virtual DOM and the old virtual DOM diff, figure out how to minimize the number of updates, and finally reflect the real DOM. Let’s take a look at the source code diagram for diff (the updateChildren function in inis.ts of the Snabbdom). Don’t worry about the details yet

We can first find through simple observation that patchVnode is interspersed in the continuous if judgment. PatchVnode () is simply a way to update nodes. In this process, we find different places through constant comparison, and then update nodes accordingly.

Okay, so that’s where we start the diff. But we’re going to keep two questions in mind as we go along

  1. How does diff find the difference between the old and new vNodes and update the old ones?
  2. And then how do you get up the tree

Diff algorithm process

Here is stolenThe React ‘s diff algorithmTo introduceAll the nodes in the diagram are connected at the same level, and our principle in diff is this. Comparisons are made within layers, not across layers. Even if it is the same virtual node, but across layers, it will not diff, delete the old node, and insert the new node.

About the key

In nodes of the same level, only the same virtual node is refined. Otherwise, the old node is deleted and the new node is inserted. So how do we know that our node is the same virtual node? The answer is to add a uniquely identified key. When we add a unique key to each node, we can update it more efficiently.

When node Z wants to insert a node between CDS

No key update

If there is no key added to the node, z will squeeze between CDS and occupy d. The next update will change d to Z, e to D, and then create e again. If fghizklMNopGrsZuvwhyz follows, then each node will need to be updated again. That is to say, vNodes are compared one by one, and if they are found to be incorrect, they will be replaced one by one. So how do we optimize

A key update

Again steal figureThe React ‘s diff algorithm

When we add a key to each of the nodes, and again insert z and c between the CDS the nodes after that are not recreated when we update them. To put it another way, the nodes after C are reused, not updated. This will be much faster, just create a z after c

Comparing the two cases in this case, we can find that when carrying the key, we can identify the unchanged part, and when adding, we only do one DOM operation to add. Without a key, the number of DOM operations we need to do depends on how long the node is behind it…

The patch function

Patch function is an entry point for us to do diff. We will start from this function and get the desired effect step by step. At the end of init.ts in snabbDOM, a patch function is returned. I will add comments to the source code to explain some of the key judgments to help with understanding. The following

return function patch(oldVnode: VNode | Element, vnode: VNode) :VNode {
   // The oldVnode type is VNode or Element, which means that our oldVnode can pass a DOM Element.
   let i: number.elm: Node, parent: Node
   const insertedVnodeQueue: VNodeQueue = []
   for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
   // Determine whether oldVnode is a virtual node or a DOM node
   if(! isVnode(oldVnode)) {// If oldVnode is not a virtual node, then oldVnode is wrapped as a virtual node with emptyNodeAt()
     oldVnode = emptyNodeAt(oldVnode)
   }
   // Use sameVnode() to check whether oldVnode is the same as the new Vnode we passed in
   if (sameVnode(oldVnode, vnode)) {
     // If so, make a refined comparison
     patchVnode(oldVnode, vnode, insertedVnodeQueue)
   } else {
     // Delete old nodes and insert new ones
     elm = oldVnode.elm!
     parent = api.parentNode(elm) as Node
	 // Create new nodes
     createElm(vnode, insertedVnodeQueue)

     if(parent ! = =null) { api.insertBefore(parent, vnode.elm! , api.nextSibling(elm))// Delete the old node
       removeVnodes(parent, [oldVnode], 0.0)}}for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data! .hook! .insert! (insertedVnodeQueue[i]) }for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
   return vnode
 }
}
Copy the code

It’s worth mentioning here that to convert vNode to DOM we use the createElm() function. However, since the Vnode has Children, we will encounter the problem of array object nesting when creating the node. I’m going to go into createElm without going into the details. But the main thing is that createElm() addresses this problem recursively by calling itself while iterating through the child nodes. I’ve labeled createElm with a lookup function for you to read, and I’ve posted the image below.

I have also posted the code for the sameVnode() function so you can see how snabbDOM determines whether two VNodes are the same

  1. The keys of vnodes are the same
  2. Vnodes have the same selectors
function sameVnode(vnode1: VNode, vnode2: VNode) :boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
Copy the code

2) ⌄ ́ the other minor functions are indeed not difficult and are to be understood by those who are interested in them.

Back to the topic, we can see from the patch function that patchVnode function is entered after two judgments. Then, we will follow the steps to enter the patchVnode function

PatchVnode function

PatchVnodez mainly tells us how the virtual DOM realizes the DOM update and what logic judgment has been made. As you go through this section, you need to think about why is this true? Why do you do that?

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    consthook = vnode.data? .hook hook? .prepatch? .(oldVnode, vnode)const elm = vnode.elm = oldVnode.elm!
    // Define the children of the old node
    const oldCh = oldVnode.children as VNode[]
    // Define the children of the new node
    const ch = vnode.children as VNode[]
    // Check whether oldVnode and vnode are the same object
    if (oldVnode === vnode) return // If yes, no operation is performed

    if(vnode.data ! = =undefined) {
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) vnode.data.hook? .update? .(oldVnode, vnode) }// Check whether the vnode has a text attribute. IsUndef checks whether the contents of the vnode are undefined
    if (isUndef(vnode.text)) {
      // If vnode.text is undefined
      // When vNode has no text attribute, it means that vNode has children

      // if oldCh and ch are not undefined
      if (isDef(oldCh) && isDef(ch)) {
        // If oldCh does not equal ch, go to updateChildren().
        if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) }else if (isDef(ch)) {
        // If Vnode has Children
        // Add Children in Vnode
        if (isDef(oldVnode.text)) api.setTextContent(elm, ' ')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // If oldVnode has Children
        // Delete Children from oldVnode
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)}else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, ' ')}// Check whether the text of oldVnode and vnode are the same
    } else if(oldVnode.text ! == vnode.text) {// Check whether oldCh exists
      if (isDef(oldCh)) {
        // If so, delete oldCh and replace it with vnode.text
        removeVnodes(elm, oldCh, 0, oldCh.length - 1) } api.setTextContent(elm, vnode.text!) } hook? .postpatch? .(oldVnode, vnode) }Copy the code

Here we can see that although we are talking about improving performance, there are still some places where we just delete the old ones and insert the new ones. The reason may be that we don’t see this very often, so it’s not necessary to compare everything in the optimization process. Now we’re going to get to the core part of the updateChildren function, where we do the diff algorithm.

UpdateChildren Function (Diff algorithm)

PatchVnode has a direct cross-reference process with updateChildren, because when we first enter patchVnode into updateChildren, we do not know whether there are children in oldVnodeChildren. Therefore, we need to add patchVnode again in the middle of each comparison, so that the child nodes of each layer with child nodes can be processed accordingly

InsertBefore inserts a child node with the specified parent node before the reference node. First, I’ll post the updateChildren function code and then go through the sections

function updateChildren(parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldStartVnode.elm! , api.nextSibling(oldEndVnode.elm!) ) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldEndVnode.elm! , oldStartVnode.elm!) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          elmToMove = oldCh[idxInOld]
          if(elmToMove.sel ! == newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!) }else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as anyapi.insertBefore(parentElm, elmToMove.elm! , oldStartVnode.elm!) } } newStartVnode = newCh[++newStartIdx] } }if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] = =null ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }
Copy the code

Diff sets four Pointers at both ends of the old and new child nodes. We record these four Pointers and the vNodes to which they point

   let oldStartIdx = 0// Point to the first Vnode of the old node
   let newStartIdx = 0// Point to the first Vnode of the new node
   let oldEndIdx = oldCh.length - 1// Points to the last Vnode of the old node
   let oldStartVnode = oldCh[0]// The first Vnode of the old node
   let oldEndVnode = oldCh[oldEndIdx]// The last Vnode of the old node
   let newEndIdx = newCh.length - 1// Points to the last Vnode of the new node
   let newStartVnode = newCh[0]// The first Vnode of the new node
   let newEndVnode = newCh[newEndIdx]// The last Vnode of the new node
Copy the code

All four Pointers move toward the center as you traverse. The loop ends when oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx

Next, we will introduce the specific movement rules of the four Pointers

Old header (oldStartVnode) —- comparison — new header (newStartVnode)

For a quick note, we use sameVnode() to determine if two VNodes are the same

  1. If the first Vnode of the new node is the same as the first Vnode of the old node, namely sameVnode(oldStartVnode, newStartVnode), patchVnode of the Vnode is made. OldStartIdx and newStartIdx are then +1, and oldStartVnode and newStartVnode are updated
      else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
    Copy the code

    The first column is newCh and the second column is oldCh

Old (oldStartVnode) — Contrast — New (newStartVnode)

  1. When the last Vnode of the new node is the same as the last Vnode of the old node, namely sameVnode(oldEndVnode, newEndVnode), patchVnode of the Vnode is made. Then oldEndIdx and newEndIdx are -1 and oldEndVnode and newEndVnode are updated
	 else if (sameVnode(oldEndVnode, newEndVnode)) {
       patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
       oldEndVnode = oldCh[--oldEndIdx]
       newEndVnode = newCh[--newEndIdx]
     }
Copy the code

Old header (oldStartVnode) — contrast —- new tail (newStartVnode)

  1. When the last Vnode of the new node is the same as the first Vnode of the old node, namely sameVnode(oldStartVnode, newEndVnode), patchVnode of the Vnode is made. Then move the newEndIdx pointer -1 and oldStartIdx pointer +1, and move oldStartVnode after oldEndVnode.
	else if(sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldStartVnode.elm! , api.nextSibling(oldEndVnode.elm!) ) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }Copy the code

What does that mean? This is when the D of the new node and the D of the old node are matched by sameVnode(oldStartVnode, newEndVnode). To change the old node on the left to the new node on the right, we move the D in the old node to the end of the oldEndVnode that oldEndIdx points to, which is the B in the figure. The oldStartIdx pointer +1 and newEndIdx pointer -1 are then added

InsertBefore () is another word here. The node.insertbefore () method inserts a child Node with the specified parent Node before the reference Node. Simply put, you add one node in front of another. And nextSibling() is the next node of one node. So we add the D above to the next node of B by insertBefore() with nextSibling(). So now we’re adding D after B which is this line of code

api.insertBefore(parentElm, oldStartVnode.elm! , api.nextSibling(oldEndVnode.elm!) )Copy the code

AppendChild is not available because we now need to add the node after oldEndVnode, not after oldCh.

Old (oldStartVnode) —- contrast — new (newStartVnode)

  1. When the end of the old node a Vnode is the same as the old new node first Vnode, namely sameVnode (oldEndVnode, newStartVnode), then the Vnode patchVnode node. Then move the oldEndIdx pointer -1 and newStartIdx pointer +1, and move oldStartVnode after oldEndVnode.
	else if(sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldEndVnode.elm! , oldStartVnode.elm!) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }Copy the code

After oldEndVnode matches newStartVnode, in order to change the old node on the left to the new node on the right, we need to put D in front of oldStartVnode (B). The oldEndIdx pointer -1 and newStartIdx pointer +1 are then added. The way to put D in front of B is to use the insertBefore() method that we just introduced

So here I have clarified the way in which the four nodes in the diff are compared. When I was learning this part of the source code, I also used PPT to draw this figure, and if I looked at the circle, I would draw myself. Now let’s go back to this piece of code

function updateChildren(parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0// The starting point of the old node
    let newStartIdx = 0// The starting point of the new node
    let oldEndIdx = oldCh.length - 1// The last pointer to the old node
    let oldStartVnode = oldCh[0]// The Vnode to which the starting point of the old node points
    let oldEndVnode = oldCh[oldEndIdx]// The last pointer of the old node points to the Vnode
    let newEndIdx = newCh.length - 1// The last pointer to the new node
    let newStartVnode = newCh[0]// The Vnode to which the starting point of the new node points
    let newEndVnode = newCh[newEndIdx]// The last pointer of the new node points to the Vnode
    let oldKeyToIdx: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
      / / old head (oldStartVnode) -- -- -- -- -- -- -- -- -- -- comparison -- -- -- -- -- -- -- -- -- -- new head (newStartVnode)
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
      / / old tail (oldStartVnode) -- -- -- -- -- -- -- -- -- -- comparison -- -- -- -- -- -- -- -- -- -- new tail (newStartVnode)
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
      / / old head (oldStartVnode) -- -- -- -- -- -- -- -- -- -- comparison -- -- -- -- -- -- -- -- -- -- new tail (newStartVnode)patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldStartVnode.elm! , api.nextSibling(oldEndVnode.elm!) ) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldEndVnode, newStartVnode)) { 
      / / old tail (oldStartVnode) -- -- -- -- -- -- -- -- -- -- comparison -- -- -- -- -- -- -- -- -- -- new head (newStartVnode)patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) api.insertBefore(parentElm, oldEndVnode.elm! , oldStartVnode.elm!) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }Copy the code

I can’t find the first four

If none of the four comparisons match, newStartVnode does not start or end in oldCh or does not exist. So we create a table of oldCh mappings using the createKeyToOldIdx() method. The value in the table is the value of the subscript for the key of each node

else {
      if (oldKeyToIdx === undefined) {
        // Create a mapping of oldCh oldKeyToIdx
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      idxInOld = oldKeyToIdx[newStartVnode.key as string]
      if (isUndef(idxInOld)) { // New element
        // If idxInOld is undefined, create a newStartVnode in front of oldStartvNode.elm
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
      } else {
        // If idxInOld is not undefined, elmToMove = elmToMove
        elmToMove = oldCh[idxInOld]
        if(elmToMove.sel ! == newStartVnode.sel) {// If elmToMove and newStartVnode have different sel tags, then we need to create a new newStartVnode before oldStartvNode.elm
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // If they are all the same, make patchVnode once. The effect has been described above and they are all similar
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined as any
          // Finally find newStartVnode in oldCh and update it to the front of oldStartVnodeapi.insertBefore(parentElm, elmToMove.elm! , oldStartVnode.elm!) } } newStartVnode = newCh[++newStartIdx] } }/ / create oldKeyToIdx
function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number) :KeyToIndexMap {
  const map: KeyToIndexMap = {}
  // Start with beginIdx and end with endIdx
  for (let i = beginIdx; i <= endIdx; ++i) {
    constkey = children[i]? .keyif(key ! = =undefined) {
      map[key] = i
    }
  }
  return map
}
Copy the code

Skip Vnode with null for the first four if checks (see next node). Enter the four sameVnode judgments and one exception. Through the source code we can see the order of the five judgments as follows

  1. Old head (oldStartVnode) — — — — — — — — — — comparison — — — — — — — — — — new head (newStartVnode)
  2. Old tail (oldStartVnode) — — — — — — — — — — comparison — — — — — — — — — — new tail (newStartVnode)
  3. Old head (oldStartVnode) — — — — — — — — — — comparison — — — — — — — — — — new tail (newStartVnode)
  4. Old tail (oldStartVnode) — — — — — — — — — — comparison — — — — — — — — — — new head (newStartVnode)
  5. I can’t find the first four

As for why is this order, may be based on the probability of various situations for the consideration of the point ~~

Process remaining nodes

So after we’ve done this cycle in this order, we’re going to have three cases

  1. OldStartIdx > oldEndIdx. In this case, the old Vnode has been traversed, but the new Vnode has not been traversed. At this point there are more new Vnod nodes than old vNodes, so we need to insert the remaining VNodes into the actual DOM nodes.
if (oldStartIdx > oldEndIdx) {
  before = newCh[newEndIdx + 1] = =null ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
Copy the code

The addNodes method just iterates over the extra bits in newCh and inserts them in front of the “before” bar

function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
 	 // Iterate over the extra parts of newCh
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      // As long as the node is not null
      if(ch ! =null) {
      // Insert before before
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
      }
    }
  }
Copy the code

Put two graphs put before not null: (ps: look at github vue source code explanation, that big guy put this picture wrong hahaha hahaha hahaha) but somebody else draw good, this read don’t understand to see his paBosses link Before is null. When before is null, insertBefore in the addVnode method automatically pushes the next selected node to the end of the queue

  1. OldStartIdx < = oldEndIdx. In this case, the new Vnode is traversed, but the old Vnode is not traversed. Therefore, at this time, there are fewer new Vnod nodes than old vNodes, so we need to delete the extra vnodes in the old node.
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
Copy the code

  1. NewCh, oldCh, that’s it. Knock it off.

conclusion

I’m done with this thing. Tell me how it feels.

  1. (My tongue twister) The authors of snabbdom are really cool for coming up with the idea of using four Pointers to identify the difference between the old node and the new node. For some reason, I kept thinking about the vernier calipers. The pointer is like two external measuring claws, and when the external measuring claw contracts, it’s like two Pointers contract to break out of the while loop. The inner claws (the outer claws shrink to a minimum, the inner claws cross) or the outer claws are stuck with meaningful data, just like the four nodes. There is also the mutual call of patchVnode and updateChildren, which not only solves the job of the two functions, but also solves the problem of array nesting by calling each other. Similarly, createElm solves this array nesting problem recursively.
  2. (My nonsense) Blogging has really helped me to accumulate knowledge. But with more than 2W words, it’s hard to draw these pictures one by one… Although the diff in the vue source code is not very different from this, the function name is similar, but vue3 diff seems to be updated…… But there’s still a lot to be gained, and hopefully these thoughts will be passed to me as randomly as the next Eldian among the giants who attack when they die
  3. It’s 1:00. Knock off. If someone sees a problem, tell them. If nobody sees a problem, forget it.