Vue源码-snabbdom

116 阅读4分钟

虚拟 DOM 和 diff 算法

  1. 虚拟 dom 如何被渲染成函数(h 函数)产生?
  2. diff 算法的原理
  3. 虚拟 dom 如何通过 diff 变成真正的 dom(虚拟 dom 变成真正的 dom.也是涵盖在 diff 算法里面的)

snabbdom 是虚拟 dom 库

diff 算法是发生在虚拟 dom 上的

h 函数

init 方法返回 path,path 可以使虚拟 dom 上树

用法

h 函数可以嵌套(会产生虚拟 dom 树) 多个子节点需要使用[],但是只有一个子节点的时候不需要使用

diff

特点

  1. key 很重要,key 是唯一的标识,告诉 diff 算法,在更改前他们是同一个节点
  2. 是同一个虚拟节点(选择器相同且 key 相同)才会 diff 算法,否则暴力删除插入新的节点
  3. 只会同层比较,不会跨层比较,同层的时候会全部对比 在 vue 的开发当中(2 3 )两个很少出现

如何定义同一个节点

源码:key 相同且选择器要相同

diff 流程

1. 判断当前老节点是不是 vnode

1.1 是的话就直接往2走
1.2 不是的话就让他变成虚拟节点

2. 判断新老虚拟节点是不是同一个节点(key 相同,且选择器相同)

2.1 是的话就精细的比较,
2.2 不是的话就直接暴力插入新的节点,删除旧的节点(为什么先插入后删除呢,是因为直接删除掉的话就没有标杆了,位置无法固确定)

注:标杆节点是为了使用 insertBefore 方法,dom 上树有两个方法,一个就是 insertBefore,一个就是 appendChild(这个就没有标杆)

对 2.1 流程的继续延伸

1. old和new在内存中是不是同一个对象
    1.1 是的话就什么都不做
    1.2 不是的话执行 2
2. new有没有text属性
    2.1 有的话,判断是否和old的text是否相同,
        2.1.1 相同就啥都不做,
        2.2.2 不相同就把old的文本改成new里面的文本(即使老的没有text也无所谓,只要不相同替换就完事了)
    2.2 没有(意味着new有children)的话,执行 3
3. old有没有children
    3.1 没有的话就意味着是有text,然后1. 清空old的text。2. 并且把new的children添加到dom上。
    3.2 有的话就是最复杂了,就是新老都有children,需要深度diff来找到差异了

对 3.2 流程的继续延伸

(对所有的算法杂糅在一起很不好)

1.新增节点

新增节点的时候插入到所有未处理节点之前,而不是所有未处理节点之后

  1. 遍历 new 的所有 children
  2. 在 new 的遍历里面遍历 old 的 children
  3. 如果 new 的 child 在 old 里面存在,就给他打个存在的标记
  4. 需要 old 也加个指针 O,在 new 里面加指针 N,避免 old 的有没有处理对比的节点

2.删除节点

  1. 把没有处理的节点都

3.更新节点

(好的算法,分开)

diff 算法更新优化策略

四种命中查找

需要四个指针,新前,新后,旧前,旧后.

  1. 新前与旧前(命中,就都往下走)
  2. 新后与旧后(命中,就都往上走)
  3. 新后与旧前(命中,此时旧需要移动新后指向的节点到老节点的旧后的后面,也就是旧前的节点没了,需要移动到旧后的后面)(新往上走,旧往下走 )
  4. 新前与旧后(命中,此时就需要移动新前指向的节点到老节点的旧前的前面,也就是旧后的节点没了,需要移动到旧前的后面)(新往下走,旧往上走 )
如果四个都没有命中,

就需要循环来找了,把新里面这个界面在旧里面去循环找,

  1. 找到的话就以为这是位置移动了(这种情况下,在旧里面找到了,那么新前就往下走)
  2. 找不到就说明之前没有这个节点,那么就会把这个新前的这个节点塞到旧前节点的上面

(命中一种就不在命中判断,命中不了就继续往下判断,旧往下移,新往上移)

循环结束条件

只有新前<=新后&&旧前<=旧后,才继续执行循环,否则就循环结束了

循环结束有剩余节点情况
  1. 如果旧的先循环结束,那么新的就是新增节点操作.那么新节点中的剩余就是要增加的节点
  2. 如果新节点先循环结束,那么新的就是删除节点操作.那么老节点中的剩余就是要删除的节点

根据上面(四种命中查找)书写更新子节点方法

  function updateChildren(
    parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue
  ) {
    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: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    // 这边是执行循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];
        // 命中 1 新前与旧前
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
        // 命中 2 新后与旧后
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
        // 命中 3 新后与旧前
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        // 不用appendchild是因为这个旧后并不一定是最后一个节点,
        // 所以这里使用的是移动节点,旧后的下一个节点
        api.insertBefore(
          parentElm,
          oldStartVnode.elm!,
          api.nextSibling(oldEndVnode.elm!)
        );
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
        // 命中 4 新前与旧后
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
        // 这里就是四种都没有命中,也就是最麻烦的
      } else {
        // 如果keyMap(oldKeyToIdx)不存在,先制作老的key的map,这样就不用每次都去使用循环的方法了
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 寻找当前这项,(newStartIdx)这项在keyMap中映射的序号关系
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        // 如果是undefined,那么说明旧的里面没有这个项,就是新加的
        if (isUndef(idxInOld)) {
          // New element 那么就创建新节点
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
        } else {    // 如果不是全新的,也不是undefined,那么就是要移动
          elmToMove = oldCh[idxInOld];
          // 如果不是同一个节点
          if (elmToMove.sel !== newStartVnode.sel) {
            // 别加入的项,就是newStartVnode这项,不过目前只是个vnode,需要创建节点
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        // 指针也要下移
        newStartVnode = newCh[++newStartIdx];
      }
    }
    // 循环结束的时候,新前还是小于新后,那说明是从旧那里结束了,那说明还有新的节点要添加(新增的情况)
    if (newStartIdx <= newEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        before,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      );
    }
    // 循环结束的时候,旧前还是小于旧后,那说明是从新那里结束了,那说明还有旧的节点要删除(删除的情况)
    if (oldStartIdx <= oldEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

总结

组件使用为 snabbdom,

AST 抽象语法树

抽象语法树的本质就是一个 JS 对象; 对象包含 tag attrs type children 等属性; 模板语法-->转成字符串-->转成 AST 语法树

模板语法--->正常的 HTML 语法(算法编写难度极大)

模板语法--->抽象语法树---->正常的 HTML 语法(算法编写难度较小)

抽象语法树和虚拟节点的关系

模板语法-->抽象语法树-->渲染函数(h 函数)-->虚拟节点---经过diff--->界面