The 13th part of VUE source code learning-patch (dom update)

1. Overview

In the previous chapter, we explained the principle of the diff algorithm. In this chapter, let's see how vue implements the patch process through this algorithm.

Remember when we talked about vm._update in the sixth chapter, it was responsible for converting Vnode into real dom, including two branch processes, the first rendering of dom, and the subsequent update of dom.

 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    ...
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    ...
  }

In the sixth chapter, we have learned the process of dom rendering first. In this chapter we continue to the second branch process.

Second, patch

We use the following dom update example, combined with source code to analyze.

 <div id="app">
    <button v-on:click="changeMsg">设置</button>
  <ul>
    <li v-for="item in items" :id="item.id">
    </li>
  </ul>
</div>


var vm = new Vue({
    el:"#app",
    data:{
      msg:'this is msg',
      items:[
      {id:1},
      {id:2},
      {id:3}
      ],
   methods:{
    changeMsg(){
      this.items.push({id:4});
    }
  }
})

This example is relatively simple, after the dom rendering is completed as follows:

When the "Settings" button is clicked, an additional column is added to the array. From the previous knowledge, it can be seen that the change of the item attribute value will trigger the update of its related subscription watcher object. Because this attribute is used in the template, it will trigger the update of the render wacher, execute vm._render (), and produce a new vnode. vm. $ el = vm .__ patch __ (prevVnode, vnode), through the comparison of new and old vnode, to update the real dom. The final rendering is as follows:

The whole process is that one li node is added, and the others remain unchanged. We expect that in the actual DOM update, only a new li node is added instead of global refresh, so that the efficiency is the highest.

The vm .__ patch __ (prevVnode, vnode) method is ultimately defined in src / core / vdom / patch.js.

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ....
     //1、oldVnode为空(首次渲染,或者组件),直接创建新的root element
    if (isUndef(oldVnode)) {//
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      //2、oldVnode存在,进入patchVnode过程
      const isRealElement = isDef(oldVnode.nodeType)//oldVnode是id为app的dom节点
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        ......
        //3、创建节点
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
       ...
  }

Into the patchVnode process, we directly look at the second step, first determine isRealElement, and sameVnode. isRealElement indicates whether it is an actual dom node, and then look at the sameVnode method.

function sameVnode (a, b) {
  return (
    a.key === b.key && (//key相同,undefined也算相同
      (
        a.tag === b.tag &&//tag相同
        a.isComment === b.isComment &&//是否是comment
        isDef(a.data) === isDef(b.data) &&//data,属性已定义
        sameInputType(a, b)//input相同
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

sameVnode is actually the comparability of the two described nodes, it needs to be satisfied (the latter or the condition is temporarily ignored),

(1) The key is the same, and undefined is also the same.

(2) The same tag, such as div, ul

(3) The data attribute, whether there are attribute definitions to be consistent. For example, the id attribute on the li node. (Note: the data attribute does not need to be the same)

(4) Input type. The type type is the same, whether data definition should be consistent.

That is to say, if the above conditions are met, it is eligible to perform patching between nodes and continue the comparison of child nodes, otherwise directly go to the else branch to create and insert a new node.

     Here we talk about the key. During the development process, when we use v-for, input and other tags, we need to add a key to indicate a unique identifier, otherwise we will find "strings" between elements and we will not get Our expected effect. In this example, we did not add the key, the key is undefined, so to judge the comparability, only the latter condition is adopted. For the li node with id = "1", "in-place multiplexing" is used. Sometimes, we do n’t want to do this, we just need to set the keys of these two elements to be different, then the comparability of these two elements is not enough, and the element node will be recreated.

The root node div in this example satisfies this condition, so enter the patchVnode process.

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
   ...

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    //1、对于static节点树,无需比较,直接节点复用
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    
    //获取oldVnode,Vnode的子节点,进行比较
    const oldCh = oldVnode.children
    const ch = vnode.children
    //2、更新当前节点,执行update相关方法
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    //3、比较子节点
    if (isUndef(vnode.text)) {//3.1 非text节点叶节点
      if (isDef(oldCh) && isDef(ch)) {//新,旧子节点存在
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {//新子节点存在,旧子节点不村子,添加新节点
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {//旧子节点存在,新子节点不存在,删除旧节点
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {//旧节点的为text节点,则设置为空
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {//3.2 text叶节点,但是text不同,直接更新
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

The core process of patchVnode is divided into three parts.

1. Determine whether it is static. In the compilation section, we introduced what is a static node. In order to improve patch efficiency, for static nodes, directly reuse elements.

2. Update the current node and execute the relevant update method. These methods are as follows, mainly to update the content defined in the data attribute. (sameVnode does not require the same reason for the data attribute)

updateAttrs(oldVnode, vnode)
updateClass(oldVnode, vnode)
updateDOMListeners(oldVnode, vnode)
updateDOMProps(oldVnode, vnode)
updateStyle(oldVnode, vnode)
update(oldVnode, vnode)
updateDirectives(oldVnode, vnode)

3. Compare child nodes.

(1) For non-text leaf nodes, continue to compare child nodes. If the new and old child nodes exist, updateChildren is called to continue the comparison; if the new child node exists, the old child node does not exist, it means that the child node is new, and it is created by calling addVnodes; if the new child node does not exist, the old child node exists, which means the child node Is redundant, call removeVnodes to delete the node.

(2) For the text leaf node, if the text content is different, it is directly updated.

In this example, the child nodes of the ul element, the new and old nodes all have li child nodes. So enter the updateChildren method.

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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, 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)) {// os没有定义,os索引向后移动一位
        oldStartVnode = oldCh[++oldStartIdx] 
      } else if (isUndef(oldEndVnode)) {//oe没有定义,oe索引向前移动一位
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {//os==ns,保持节点位置不变,继续比较其子节点,os,ns索引向后移动一位
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {//oe==ne,保持节点位置不变,继续比较其子节点,oe,ne索引向后前移动一位。
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // os==ne,将os节点移动到oe后面,继续比较其子节点,os索引向后移动一位,ne索引向前移动一位
        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)) { // oe==ns,将oe移动到os节点前,继续比较其子节点,oe索引向后移动一位,ns向前移动一位。
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {//两组都不相同的情况下
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        //在oldstartIdx与oldEndIdx间,查找与newStartVnode相同(key相同,或者节点元素相同)节点索引位置
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          //没有相同节点,则将newStartVnode插入到oldStartVnode前
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          //key值和节点都都相同
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            //移动到oldStartVnode前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            //相同的key,但节点元素不同,创建一个新的newStartVnode节点,插入到oldStartVnode前。
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        //newStartVnode索引加1
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {//旧节点先遍历完,新增剩余的新节点,并插入到ne+1前位置
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {//新节店先遍历完,删除剩余的老节点
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

The updateChildren method is the realization of the diff method, and you can read the code in accordance with the diagram in the previous chapter.

Initialize the beginning of the new and old nodes, and end the index. Represented by os (oldStartIdx), oe (oldEndIdx), ns (newStartIdx), ne (newEndIdx) respectively.

(1) os is not defined, and the os index is moved back one bit.

(2) oe is not defined, and the oe index moves forward one bit.

(3) os == ns, keep the node position unchanged, continue to compare its child nodes, os, ns index moves one bit backward.

(4) oe == ne, keep the node position unchanged, continue to compare its child nodes, the index of oe, ne moves one bit backward and forward.

(5) os == ne, continue to compare its child nodes, move the os node behind oe, the os index moves backward one bit, and the ne index moves forward one bit.

(6) oe == ns, continue to compare its child nodes, move oe to os node, oe index moves one bit backward, and ns moves one bit forward.

(7) When the two groups are not the same, between os and oe, find the node that is the same as newStartVnode (the same key or the same node element), if the node does not exist, create a new node and insert it before os ; If the node exists, move the node to os. The ns index is moved back one bit.

(8) The old node is traversed first, the remaining new node is added, and inserted into the position before ne + 1.

(9) The new node is traversed first, and the remaining old nodes are deleted.

In this example, the li node, the first three of the new and old nodes are the same, and the last one is added, so the eighth case of execution meets our expectations.

Among them, in the case of 3,4,5,6, call patchVnode to update the node, and continue to compare the child nodes until it reaches the leaf node.

3. Summary

The whole process is to map the actual dom operation through the comparison between the new and old Vnode and the diff algorithm.

Published 33 original articles · Liked 95 · Visitors 30,000+

Guess you like

Origin blog.csdn.net/tcy83/article/details/94329331
Recommended