【Vue2深度学习】虚拟DOM篇-Patch主流程

899 阅读7分钟

虚拟DOM最核心的部分是patch,它通过Vue-Diff算法,比对新旧两个vnode之间有哪些不同,然后根据比对结果找出需要更新的节点进行更新,最终将vnode渲染成真实的DOM。整个patch的过程,其实就是创建节点、删除节点和修改节点的过程。

创建节点

当因状态改变而新增的节点在DOM中并不存在时,我们需要创建一个节点并插入到DOM中。即当oldVnode不存在而vnode存在时,就需要使用vnode生成真实的DOM元素并将其插入到视图当中。

创建新增的节点主要有以下两种场景:

场景1:

首次渲染时,DOM中不存在任何节点,即oldVnode是不存在的,我们需要使用vnode创建一个新DOM节点并渲染视图。

场景2:

当vnode 和oldVnode完全不是同一个节点时,即oldVnode是一个被废弃的节点,vnode是一个全新的节点,此时,我们需要使用vnode创建一个新DOM节点,用它去替换oldVnode所对应的真实DOM节点。

  • 如何创建节点

因vnode是有类型的,所以当我们在创建节点时,最重要的事是根据vnode的类型来创建出相同类型的DOM元素。

事实上,只有三种类型的节点会被创建并插入到DOM中:元素节点、注释节点和文本节点。

  1. 创建元素节点

    判断vnode是否是元素节点,只需要判断它是否具有tag属性。如果一个vnode具有tag属性,就认为它是元素节点,然后我们调用当前环境下的createElement方法来创建真实的元素节点。

  2. 创建注释节点

    当发现一个vnode的tag属性不存在时,可以用isComment属性来判断它是注释节点还是文本节点。如果是注释节点,则调用当前环境下的createComment方法来创建真实的注释节点。

  3. 创建文本节点

    当判断出来的是文本节点,则调用当前环境下的createTextNode方法来创建真实的文本节点。

源码如下:

function createElm (vnode, parentElm, refElm) {
    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
        vnode.elm = nodeOps.createElement(tag, vnode)   // 创建元素节点
        createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
        insert(parentElm, vnode.elm, refElm)       // 插入到DOM中
    } else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)  // 创建注释节点
      insert(parentElm, vnode.elm, refElm)           // 插入到DOM中
    } else {
      vnode.elm = nodeOps.createTextNode(vnode.text)  // 创建文本节点
      insert(parentElm, vnode.elm, refElm)           // 插入到DOM中
    }
  }
​

当一个元素节点被创建后,接下来要做的事情就是将它插入到指定的父节点中。我们只要调用当前环境下的appendChild方法,就可以将一个元素插入到指定的父节点中。如果这个指定的父节点已经被渲染到视图,那么把元素插入到它的下面将会自动将元素渲染到视图。

删除节点

因为渲染视图时,需以vnode为标准,所以vnode中不存在的节点都属于被废弃的节点,需要从DOM中删除。

在上述场景2中,当vnode 和oldVnode完全不是同一个节点时,在DOM中需要使用vnode创建新节点替换oldVnode所对应的旧节点,而替换的过程就是将新创建的DOM节点插入到旧节点的旁边,然后再将旧节点删除。

  • 如何删除节点

    删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可。源码如下

    function removeNode (el) {
        const parent = nodeOps.parentNode(el)  // 获取父节点
        if (isDef(parent)) {
          nodeOps.removeChild(parent, el)  // 调用父节点的removeChild方法
        }
      }
    
更新节点

无论是新增节点,还是删除节点,他们之间都有一个共同点,那就是两个虚拟节点是完全不同的。

而当新旧两个节点是相同的节点时,我们需要对这两个节点进行比较细致的比对,然后对oldVnode在视图中所对应的真实节点进行更新。

例如,当新旧两个节点是同一个文本节点而其中的文本不一样时,我们就需要重新设置oldVnode在视图中所对应的真实DOM节点的文本。

  • 如何更新节点

    在介绍如何更新节点之前,我们先来了解一个概念: 静态节点

    所谓静态节点,就是指那些一旦渲染到界面上之后,无论日后状态如何变化,都不会发生任何变化的节点。比如:

    <p>我是不会变化的文字</p>
    

    了解静态节点之后,我们开始更新节点。更新节点需要对以下3种情况进行判断并分别处理。

    1. 如果VNodeoldVNode均为静态节点

      直接跳过,无需处理。

    2. 如果VNode是文本节点

      如果VNode是文本节点即表示这个节点内只包含纯文本,那么只需看oldVNode是否也是文本节点,如果是,那就比较两个文本是否不同,如果不同则把oldVNode里的文本改成跟VNode的文本一样。如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把它改成文本节点,并且文本内容跟VNode相同。

    3. 如果VNode是元素节点

      如果VNode是元素节点,则又细分以下两种情况

      • 该节点包含子节点

        如果新的节点内包含了子节点,那么此时要看旧的节点是否包含子节点,如果旧的节点里也包含了子节点,那就需要递归对比更新子节点;如果旧的节点里不包含子节点,那么这个旧节点有可能是空节点或者是文本节点,如果旧的节点是空节点就把新的节点里的子节点创建一份然后插入到旧的节点里面,如果旧的节点是文本节点,则把文本清空,然后把新的节点里的子节点创建一份然后插入到旧的节点里面。

      • 该节点不包含子节点

        如果该节点不包含子节点,同时它又不是文本节点,那就说明该节点是个空节点,那就好办了,不管旧节点之前里面都有啥,直接清空即可。

    源码如下:

    function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
      // vnode与oldVnode是否完全一样?若是,退出程序
      if (oldVnode === vnode) {
        return
      }
      const elm = vnode.elm = oldVnode.elm
    ​
      // vnode与oldVnode是否都是静态节点?若是,退出程序
      if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
      ) {
        return
      }
    ​
      const oldCh = oldVnode.children
      const ch = vnode.children
      // vnode有text属性?若没有:
      if (isUndef(vnode.text)) {
        // vnode的子节点与oldVnode的子节点是否都存在?
        if (isDef(oldCh) && isDef(ch)) {
          // 若都存在,判断子节点是否相同,不同则更新子节点
          if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        }
        // 若只有vnode的子节点存在
        else if (isDef(ch)) {
          /**
           * 判断oldVnode是否有文本?
           * 若没有,则把vnode的子节点添加到真实DOM中
           * 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
           */
          if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
          addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        }
        // 若只有oldnode的子节点存在
        else if (isDef(oldCh)) {
          // 清空DOM中的子节点
          removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        }
        // 若vnode和oldnode都没有子节点,但是oldnode中有文本
        else if (isDef(oldVnode.text)) {
          // 清空oldnode文本
          nodeOps.setTextContent(elm, '')
        }
        // 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
      }
      // 若有,vnode的text属性与oldVnode的text属性是否相同?
      else if (oldVnode.text !== vnode.text) {
        // 若不相同:则用vnode的text替换真实DOM的文本
        nodeOps.setTextContent(elm, vnode.text)
      }
    }
    
小结

综上可知,整个patch主流程并不复杂。当oldVnode不存在时,直接使用vnode渲染视图;当oldVnode和vnode都存在但并不是同一个节点时,使用vnode创建的DOM元素替换旧的DOM元素;当oldVnode和vnode是同一个节点时,使用更详细的对比操作对真实的DOM节点进行更新。

QQ截图20220527161553.png