diff.jpeg

Virtual DOM

为什么要使用Virtual DOM?

  • 将得到的变更通知生成新的Virtual DOM树。将新的和旧的进行diff patch操作,减少了直接通过DOM API去增删改查DOM的操作,提高开发效率。
  • All problem in computer science can be resolved by another level of indirection 软件开发中的所有问题都可以通过增加一层抽象来解决。(关注点分离)

Virtual DOM是分层思想的一种体现

  • 框架将DOM抽象成Virtual DOM后可以应用在各个终端

Virtual DOM.jpeg

Diff策略

1、 按tree层级diff(level by level)

  • 在Web UI中很少会出现DOM层级会因为交互而产生更新
  • 在新旧节点之间按层级进行diff

tree-diff.jpeg

2、 按类型进行diff

  • 不同类型的节点之间往往差异很大,为了提升效率,只会对相同类型节点进行diff
  • 不同类型会直接创建新类型节点,替换旧类型节点
  • 下图中,由上一层图形变为下一层图形。同层比较,第二列五角星和三角形不同,虽然子节点的两个五角星相同,但是也会直接将三个五角星直接销毁,替换为新的节点。

不同类型-1.jpeg
不同类型-2.jpeg

3、 列表Diff

  • 给列表元素设置key,可以提升效率

Diff过程

  updateChildren(parentElm, oldCh, newCh) {
    let oldStartIdx = 0, 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
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {   // 对于vnode.key的比较,会把oldVnode = null
        oldStartVnode = oldCh[++oldStartIdx]
      } 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)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        patchVnode(oldStartVnode, newEndVnode)
        api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        patchVnode(oldEndVnode, newStartVnode)
        api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 使用key时的比较
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
        }
        idxInOld = oldKeyToIdx[newStartVnode.key]
        if (!idxInOld) {
          api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
          newStartVnode = newCh[++newStartIdx]
        }
        else {
          elmToMove = oldCh[idxInOld]
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
          } else {
            patchVnode(elmToMove, newStartVnode)
            oldCh[idxInOld] = null
            api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
          }
          newStartVnode = newCh[++newStartIdx]
        }
      }
    }
    if (oldStartIdx > oldEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
      addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
  • updateChildren代码分析
  1. 循环成立条件:OldStart小于等于OldEnd && NewStart小于等于NewEnd时
  2. 判断VNode是否为null,是指针指向下一个节点
  3. 不是则按照 OS和NS(S|)、OE和NE(E|)、 OS和NE(\ )、OE和NS(/)的顺序进行比较判断
    若相同

     S| 节点位置不变,指针+1
     E| 节点位置不变,指针-1
     \  OldStart移动到OldEnd之后,OldStart +1,  NewEnd -1
     /  OldEnd移动到OldStart之前,NewStart +1, OldEnd -1

    若不同,根据key生成OldStart和OldEnd之间的index表,查找元素是否在OldStart和OldEnd之间

     若有,直接移动到OldStart前
     若没有,创建后,移动到OldStart前
    
  4. 判断如果NewStart > NewEnd,说明新节点已经遍历完,删除OldStart和OldEnd之间的DOM
    如果OldStart > OldEnd,说明旧节点已经遍历完,将多的新节点根据index添加到DOM中去
  • 例:如图,灰色表示Virtual DOM 深色表示真实的DOM

diff-2.jpeg

  • 四个指针

    • OldStartIdx 旧开始节点
    • OldEndIdx和 旧结束节点
    • NewStartIdx 新开始节点
    • NewEndIdx 新结束节点
  1. 判断OldStartIdx和NewStartIdx是否相同。1和1相同,两个Start指针都往右移动一位(+1)
  2. 继续比较OldStartIdx和NewStartIdx是否相同。2和5不同,改为比较OldEndIdx和NewEndIdx
  3. 6和6相同,两个End指针都往左移动一位(-1)。5和2不一致,改为比较OldStartIdx和NewStartIdx
  4. 2和5不同,改为比较OldEndIdx和NewEndIdx,也不同。改为比较OldStartIdx和NewEndIdx(方向)。2和2相同,将OldStartIdx对应的真实DOM移动到OldEndIdx之后,同时OldStartIdx右移一位,NewEndIdx左移一位。
  5. 继续比较Start,不同,比较End,也不同。比较OldStart和NewEnd,不同。比较OldEnd和NewStart(/方向),相同。移动OldEnd对应的真实DOM到OldStart之前。同时OldEnd左移,NewStart右移。
  6. 继续循环,Start| End| / 四个方向,都不同。根据key生成OldStart和OldEnd之间的index表,查找7是否在OldStart和OldEnd之间。如果找到,直接挪到OldStart之前。找不到,则说明是新节点,将7由VirtualDOM生成真实DOM后挪到OldStart之前。
  7. NewEnd左移,此时NewEnd小于NewStart,结束循环
  8. 删除OldStart和OldEnd之间的部分
  9. 新Virtual DOM已存在,DOM节点也已生成,销毁旧Virtual DOM列表
  10. 设置key之后,就不要遍历了。算法复杂度为O(n),否则最坏情况为$O(n^2)$
  • vue2+中并没有完整的patch过程,节点操作是在diff操作过程中同时进行的,提升了增删改查DOM节点时的效率

CSep27
37 声望1 粉丝

学习中...整理中...