Vue source code learning - Virtual DOM+Diff algorithm

The virtual DOM + Diff algorithm is adopted in Vue, which reduces the number of operations on DOM and greatly improves the performance. Today, let's talk about the implementation logic of this part of Vue in detail. I hope it can help the little partners who do not understand this part understand this part and play it purely by hand. I hope you can give me some praise and support!

First of all, we need to make it clear that vnode represents the newly generated virtual node after this modification, and oldVnode represents the virtual node corresponding to the current real DOM structure. Therefore, our update is based on vnode and operates the real DOM through the structure of oldVnode. Vnode and oldVnode will not be changed, but only the real DOM structure will be changed.

patch

Vue will call when rendering and updating data for the first time_ Update method, and_ The core of the update method is to call VM__ patch__ Method, let's start with the patch method. The logic in the patch method is complex. Here we only look at the main process. The general process is as follows:

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    if (isUndef(oldVnode)) {
      /*oldVnode If it is not defined, create a new node*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      /*Mark whether the old VNode has nodeType*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        /*Modify the existing node directly when it is the same node*/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          oldVnode = emptyNodeAt(oldVnode)
        }
        // Insert next to the old node in the view
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        if (isDef(parentElm)) {
          /*Remove old nodes*/
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          /*Call destroy hook*/
          invokeDestroyHook(oldVnode)
        }
      }
    }

    return vnode.elm
}

Here is an isRealElement to identify whether the oldVnode has a nodeType. If there is a nodeType, it means that it is a real dom node, which should be converted to vNode through emptyNodeAt.

Two important methods are used in patch. One is createElm and the other is patchVnode.

createElm

createElm is used to create a real DOM through a virtual node and insert it into its parent node. The main process is as follows:

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    /*Create a component node*/
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode) // Create a dom node
      setScope(vnode)

      if (__WEEX__) {
           // ...
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

createComponent returns true only when vnode is a component. This part will be analyzed later.

createChildren actually traverses the child virtual nodes and recursively calls createElm. In the traversal process, vnode Elm is passed in as the DOM node of the parent container. Because it is a recursive call, the child elements will call insert first, so the insertion order of the whole vnode tree node is child first and then parent.

patchVnode

The main processes and codes of patchVnode are as follows:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*If two VNode nodes are the same, they will be returned directly*/
    if (oldVnode === vnode) {
      return
    }
    /*
      If both old and new vnodes are static and their key s are the same (representing the same node),
      And the new VNode is clone or marked with once (marked with v-once attribute, only rendered once),
      Then you only need to replace elm and componentInstance.
    */
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
      vnode.elm = oldVnode.elm
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    
    /*If this VNode node does not have text*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        /*If both old and new nodes have child nodes, diff the child nodes and call updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        /*If the old node has no child nodes and the new node has child nodes, clear the text content of elm first, and then add child nodes for the current node*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /*When the new node has no child nodes and the old node has child nodes, remove all ele's child nodes*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /*When the new and old nodes have no child nodes, it is only the replacement of text. Because the new node text does not exist in this logic, the text of ele is directly removed*/
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      /*When the text of new and old nodes is different, this text will be replaced directly*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    
  }

In fact, there are several situations:

  1. Both old and new vnode s have child nodes. diff the child nodes.
  2. There are new vnodes, there are no old vnodes, and new ones are added.
  3. There are old vnodes and no new vnodes. Delete them.
  4. If there is a text attribute, replace the text.

Mainly look at the updateChildren method invoked.

updateChildren

In the whole process of patchVnode, the most complex is the updateChildren method, which compares the new and old virtual node sub nodes.

How to compare the new and old child node lists? Very simple, cycle! Cycle the list of new child nodes, and each item in the list of old child nodes will be searched and processed. But usually, not all child nodes will change their positions.

for instance 🌰, Now there is a list. Most of our operations on it are to add, delete or change one of them. The location of most nodes is unchanged and predictable. There is no need to search circularly every time. Therefore, a faster search method is used in Vue, which greatly improves the performance. In short, it is a head to tail comparison:

  • New header: the first unprocessed node in the new child node list
  • Old header: the first of all unprocessed nodes in the old child node list
  • New tail: the last unprocessed node in the new child node list
  • Old tail: the last unprocessed node in the old child node list

Vue uses four variables newStartIdx, newEndIdx, oldStartIdx and oldEndIdx to identify the old and new head and tail nodes,

1. New head and old head

If the new header and the old header are the same node, their positions are the same, so you only need to update the node without moving.

2. New tail and old tail

If the new tail and the old tail are the same node, their positions are the same, so you only need to update the node without moving.

3. New tail and old head

If the new tail and the old head are the same node, because their positions are different, they should be moved in addition to updating.

First of all, we need to make it clear that the update is based on vnode. oldVnode represents the real DOM structure. Therefore, when we want to update the real DOM, we actually update oldVnode.

Here, we should note that it must be moved behind all unprocessed nodes, because the new tail is the last unprocessed node in the new child node list.

4. New head and old tail

If the new head and the old tail are the same node, because their positions are different, they should be moved in addition to updating.
The move logic and the new tail above are roughly the same as the old head. Move the old tail before all unprocessed nodes.

If the same node is not found in the old child node list after these four comparisons, first use oldVnode to generate a hash table {key0: 0, key1: 1} with the key of oldVnode and the value of the corresponding subscript, and then use the key of newstartvnode to search in the hash table. If the corresponding node is found, judge whether they are sameVnode:

  1. If yes, it means that the same node is found in the old child node list and the node is updated. Finally, the old node must be assigned undefined to avoid repeated comparison with the same key in the future.
  2. If not, the label may be different, or the type of input may be changed. At this time, a new node is directly created.

If the corresponding node is not found in the hash table, it is simple to create a new node directly. At the end of traversal, there are two situations:

  1. Oldstartidx > oldendidx indicates that the traversal of the old nodes is completed, and the remaining new nodes are to be added. Then they are created one by one and added to the real Dom.
  2. Newstartidx > newendidx indicates that the traversal of the new node is completed, and the remaining old nodes are to be deleted. You can delete them.

The logic is clear. Do you think it's also very simple!

ending

I'm Zhou Xiaoyang, a front-end Mengxin. I wrote this article to record the problems encountered in my daily work and the contents of my study, so as to improve myself. If you think this article is useful to you, please praise and encourage me~

Keywords: Javascript Front-end Vue.js

Added by Murciano on Fri, 25 Feb 2022 17:24:08 +0200