乐趣区

关于前端:Vue源码学习虚拟DOMDiff算法

Vue 中采纳了 虚构 DOM + Diff 算法 缩小了对 DOM 的操作次数,大大提高了性能,那么咱们明天就来具体的讲一下 Vue 中这一部分的实现逻辑,心愿能够帮忙还不了解这部分的小伙伴了解这一部分,纯手打,心愿各位小伙伴点个赞反对一下!


首先咱们要明确的是,vnode 代表本次批改后新生成的虚构节点,oldVnode 代表目前实在 DOM 构造所对应的虚构节点。所以咱们更新是以 vnode 为基准,通过 oldVnode 的构造去操作实在 DOM,vnode 和 oldVnode 都不会被扭转,被扭转的只有实在 DOM 构造。

patch

Vue 在首次渲染和数据更新的时候,会去调用 _update 办法,而 _update 办法的外围就是去调用 vm.__patch__ 办法,那么咱们就先从 patch 办法动手。patch 办法中的逻辑比较复杂,这里咱们只看次要流程局部,大抵流程如下图:

function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {if (isUndef(oldVnode)) {
      /*oldVnode 未定义的时候,创立一个新的节点 */
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      /* 标记旧的 VNode 是否有 nodeType*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        /* 是同一个节点的时候间接批改现有的节点 */
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {if (isRealElement) {oldVnode = emptyNodeAt(oldVnode)
        }
        // 插入到视图中旧节点的旁边
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        if (isDef(parentElm)) {
          /* 移除老节点 */
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          /* 调用 destroy 钩子 */
          invokeDestroyHook(oldVnode)
        }
      }
    }

    return vnode.elm
}

这里有个 isRealElement 用来标识 oldVnode 是否有 nodeType,有 nodeType 就阐明这是个真是的 dom 节点,要通过 emptyNodeAt 转化为 vNode。

patch 中用到了两个比拟重要而办法,一个是createElm,另一个是patchVnode

createElm

createElm 的作用是通过虚构节点创立实在的 DOM 并插入到它的父节点中,次要流程如下:

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    /* 创立一个组件节点 */
    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) // 创立 dom 节点
      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 只有在 vnode 是组件的时候才会返回 true,这部分当前再剖析。

createChildren 实际上是遍历子虚构节点,递归调用 createElm,遍历过程中会把 vnode.elm 作为父容器的 DOM 节点传入,因为是递归调用,子元素会优先调用 insert所以整个 vnode 树节点的插入程序是先子后父

patchVnode

patchVnode 的次要流程和次要代码如下:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /* 两个 VNode 节点雷同则间接返回 */
    if (oldVnode === vnode) {return}
    /*
      如果新旧 VNode 都是动态的,同时它们的 key 雷同(代表同一节点),并且新的 VNode 是 clone 或者是标记了 once(标记 v -once 属性,只渲染一次),那么只须要替换 elm 以及 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
    
    /* 如果这个 VNode 节点没有 text 文本时 */
    if (isUndef(vnode.text)) {if (isDef(oldCh) && isDef(ch)) {
        /* 新老节点均有 children 子节点,则对子节点进行 diff 操作,调用 updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        /* 如果老节点没有子节点而新节点存在子节点,先清空 elm 的文本内容,而后为以后节点退出子节点 */
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /* 当新节点没有子节点而老节点有子节点的时候,则移除所有 ele 的子节点 */
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /* 当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点 text 不存在,所以间接去除 ele 的文本 */
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      /* 当新老节点 text 不一样时,间接替换这段文本 */
      nodeOps.setTextContent(elm, vnode.text)
    }
    
  }

其实也就几种状况:

  1. 新旧 vnode 都有子节点,对子节点进行 diff。
  2. 新 vnode 有,旧 vnode 没有,新增。
  3. 旧 vnode 有,新 vnode 没有,删除。
  4. 如果有 text 属性,替换文本。

次要来看其中调用的 updateChildren 办法。

updateChildren

在整个 patchVnode 过程中,最简单的就是 updateChildren 办法了,这个办法里进行了新旧虚构节点子节点的比照。

如何比照新旧子节点列表呢?很简略,循环!循环 新子节点列表 ,其中每一项再去 旧子节点列表 里循环查找,而后做解决。但通常状况下,并不是所有子节点的地位都会产生扭转。

举个例子🌰,当初有一个列表,咱们对它的操作大多数就是新增一项,删除一项或者扭转其中的一项,大多数节点的地位是不变的,是 可预测的 ,没必要每次都去循环查找,所以 Vue 中应用了一种更快捷的查找形式,大大提高了性能,简略来说就是 头尾比拟

  • 新头:新子节点列表中所有 未解决 的第一个节点
  • 旧头:旧子节点列表中所有 未解决 的第一个节点
  • 新尾:新子节点列表中所有 未解决 的最初一个节点
  • 旧尾:旧子节点列表中所有 未解决 的最初一个节点

Vue 中用四个变量 newStartIdxnewEndIdxoldStartIdxoldEndIdx来标识新旧头部与尾部节点,

1. 新头与旧头

如果新头与旧头是同一节点,它们的地位也一样,所以只需更新节点即可,没有挪动操作。

2. 新尾与旧尾

如果新尾与旧尾是同一节点,它们的地位也一样,所以只需更新节点即可,没有挪动操作。

3. 新尾与旧头

如果新尾与旧头是同一节点,因为它们地位不一样,所以除了更新,还要进行挪动操作。

首先咱们要明确的是更新是以 vnode 为基准,oldVnode 代表的就是实在 DOM 构造,所以咱们要更新实在 DOM 其实就是去更新 oldVnode。

这里咱们要留神,肯定是挪动到所有 未解决节点 前面,因为新尾是新子节点列表里 未解决 的最初一个。

4. 新头与旧尾

如果新头与旧尾是同一节点,因为它们地位不一样,所以除了更新,还要进行挪动操作。
挪动逻辑和下面的 新尾与旧头 大致相同,把旧尾挪动到所有 未解决节点之前


如果通过这四次比照还是没有在 旧子节点列表 中找到雷同的节点,那么先用 oldVnode 生成一个 key 为 oldVnode 的 key,value 为对应下标的哈希表 {key0: 0, key1: 1},而后用 新头(newStartVnode) 的 key 在哈希表里查找,如果找到对应的,判断他们是不是 sameVnode

  1. 如果是,那么就示意在 旧子节点列表 中找到了雷同的节点,进行更新节点操作,最初还要将这个老节点赋值 undefined,防止后续有雷同的 key 反复比照。
  2. 如果不是,那么就有可能是标签不一样了,或者 input 的 type 扭转了,这时间接创立一个新的节点。

如果在哈希表里没找到对应的,就简略了,间接创立一个新的节点。最初遍历完结,会有两种状况:

  1. oldStartIdx > oldEndIdx,阐明老节点遍历完结,那么残余新节点就是要新增的,那么一个一个创立进去退出到实在 Dom 中。
  2. newStartIdx > newEndIdx,阐明新节点遍历完结,那么残余的老节点就是要删除的,删除即可。

逻辑理分明了,是不是感觉也很简略呢!

结尾

我是周小羊,一个前端萌新,写文章是为了记录本人日常工作遇到的问题和学习的内容,晋升本人,如果您感觉本文对你有用的话,麻烦点个赞激励一下哟~

退出移动版