美文网首页前端行者
[记录]我的Diff算法学习路径

[记录]我的Diff算法学习路径

作者: 是ADI呀 | 来源:发表于2020-11-13 10:53 被阅读0次

    Catalog

    1. Vue.js
      1.1 patch 流程
      1.2 updateChildren 大概步骤
      1.3 updateChildren 代码实现
      1.4 两端比对 DEMO
    2. React.js
      2.1 updateChildren 大概步骤
      2.2 updateChildren 代码实现
      2.3 节点移动、新增 、移除 DEMO
    3. Reference

    Vue.js Diff 算法

    patch 流程

    - oldVNode 判断 是否存在
      - oldVNode 存在
        - VNode 判断 是否存在
          - VNode 存在
            - patch
              - sameVnode 判断 key && tag && isComment && isDef(a.data) && sameInputType(a, b) 基本属性是否相同
                - sameVnode 相同
                  - diff
                    - VNode 判断 是否是文本节点
                      - 是,若 oldVNode.text !== VNode.text 则直接进行文本节点替换
                      - 否,进入子节点 diff
                        - oldCh 不存在,cn 存在,清空 oldVNode 的文本节点,同时调用 addVnodes 方法将 cn 添加到 elm 真实的 dom 节点中
                        - oldCh 节点存在,cn 不存在,则删除 elm 真实节点下的 oldCn 子节点
                        - oldCh 存在,cn 存在,调用 updateChildren 对子节点进行 diff
                        - oldVNode 有文本节点,vnode 没有,那么就清空这个文本节点
                - sameVnode 不相同
                  - 删除老的 dom 节点,生成新的 dom
          - VNode 不存在
            - 移除 DOM
      - oldVNode 不存在
        - 挂载 DOM
    

    updateChildren 大概步骤

    1. 建立两组首尾索引和节点变量
    2. 当满足 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx 条件时进入循环
      1. 双端比较 => 循环中先进行比较找出 key 相同的新旧节点,根据四种情况判断是否移动 DOM 和增减首尾索引变量以及 patch
      2. 有 key 时 => 当双端匹配没有找到 key 相同的节点,通过当前 newStartVNode 的 key 在 nextChildren 中寻找 key 相同的 VNode
        1. key 相同的 VNode 存在时,对当前节点进行移动 DOM、patch 并将旧 children 中该位置的节点设为 undefined,将 newStartIdx 下移一位
        2. key 相同的 VNode 不存在,则当前“第一个”节点为新节点,将其挂载到 oldStartVNode 之前,将 newStartIdx 下移一位
    3. 挂载全新的节点
      1. 当 oldEndIdx < oldStartIdx, 添加新节点
    4. 移除不存在的节点
      1. 当 newEndIdx < newStartIdx, 移除不存在的元素

    updateChildren 代码实现

    // 建立两组首尾索引
    let oldStartIdx = 0;
    let oldEndIdx = prevChildren.length - 1;
    let newStartIdx = 0;
    let newEndIdx = nextChildren.length - 1;
    // 索引对应的VNode
    let oldStartVNode = prevChildren[oldStartIdx];
    let oldEndVNode = prevChildren[oldEndIdx];
    let newStartVNode = nextChildren[newStartIdx];
    let newEndVNode = nextChildren[newEndIdx];
    // 进行执行双端比较-简略流程版
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVNode.key === newStartVNode.key) {
        // 步骤一:oldStartVNode 和 newStartVNode 比对
        ...
      } else if (oldEndVNode.key === newEndVNode.key) {
        // 步骤二:oldEndVNode 和 newEndVNode 比对
        ...
      } else if (oldStartVNode.key === newEndVNode.key) {
        // 步骤三:oldStartVNode 和 newEndVNode 比对
        ...
      } else if (oldEndVNode.key === newStartVNode.key) {
        // 步骤四:oldEndVNode 和 newStartVNode 比对
        ...
      }else {
        // 当双端比对没有匹配节点时,遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
      }
    }
    // 进行执行双端比较-详细流程版
    // 处理捕获,移动DOM,增减序号
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (!oldStartVNode) {
        // 当旧VNode已被diff移到别的位置时,旧VNode节点为undefined需要跳过当前索引
        oldStartVNode = prevChildren[++oldStartIdx]
      } else if (!oldEndVNode) {
        // 当旧VNode已被diff移到别的位置时,旧VNode节点为undefined需要跳过当前索引
        oldEndVNode = prevChildren[--oldEndIdx]
      } else if (oldStartVNode.key === newStartVNode.key) {
        // 步骤一:oldStartVNode 和 newStartVNode 比对
        // 调用 patch 函数更新
        patch(oldStartVNode, newStartVNode, container)
        // 更新索引,指向下一个位置
        oldStartVNode = prevChildren[++oldStartIdx]
        newStartVNode = nextChildren[++newStartIdx]
      } else if (oldEndVNode.key === newEndVNode.key) {
        // 步骤二:oldEndVNode 和 newEndVNode 比对
        // 调用 patch 函数更新
        patch(oldEndVNode, newEndVNode, container)
        // 更新索引,指向下一个位置
        oldEndVNode = prevChildren[--oldEndIdx]
        newEndVNode = nextChildren[--newEndIdx]
      } else if (oldStartVNode.key === newEndVNode.key) {
        // 步骤三:oldStartVNode 和 newEndVNode 比对
        // 调用 patch 函数更新
        patch(oldStartVNode, newEndVNode, container)
        // 将 oldStartVNode.el 移动到 oldEndVNode.el 的后面,也就是 oldEndVNode.el.nextSibling 的前面
        container.insertBefore(
          oldStartVNode.el,
          oldEndVNode.el.nextSibling
        )
        // 更新索引,指向下一个位置
        oldStartVNode = prevChildren[++oldStartIdx]
        newEndVNode = nextChildren[--newEndIdx]
      } else if (oldEndVNode.key === newStartVNode.key) {
        // 步骤四:oldEndVNode 和 newStartVNode 比对
        // 先调用 patch 函数完成更新
        patch(oldEndVNode, newStartVNode, container)
        // 更新完成后,将容器中最后一个子节点移动到最前面,使其成为第一个子节点
        container.insertBefore(oldEndVNode.el, oldStartVNode.el)
        // 更新索引,指向下一个位置
        oldEndVNode = prevChildren[--oldEndIdx]
        newStartVNode = nextChildren[++newStartIdx]
      } else {
        // 当双端比对没有匹配节点时,遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
        const idxInOld = prevChildren.findIndex(
          node => node.key === newStartVNode.key
        )
        if (idxInOld >= 0) {
          // vnodeToMove 就是在旧 children 中找到的节点,该节点所对应的真实 DOM 应该被移动到最前面
          const vnodeToMove = prevChildren[idxInOld]
          // 调用 patch 函数完成更新
          patch(vnodeToMove, newStartVNode, container)
          // 把 vnodeToMove.el 移动到最前面,即 oldStartVNode.el 的前面
          container.insertBefore(vnodeToMove.el, oldStartVNode.el)
          // 由于旧 children 中该位置的节点所对应的真实 DOM 已经被移动,所以将其设置为 undefined
          prevChildren[idxInOld] = undefined
        } else {
          // 挂载新节点,当双端比对与key查找都不匹配时 newStartVNode 即是新节点
          // newStartVNode 是这轮比较中的“第一个”节点,所以把它挂在到 oldStartVNode 前即可
          mount(newStartVNode, container, false, oldStartVNode.el)
        }
        // 将 newStartIdx 下移一位
        newStartVNode = nextChildren[++newStartIdx]
      }
    }
    // 挂载没有被处理的全新节点
    if (oldEndIdx < oldStartIdx) {
      // 循环结束后当 oldEndIdx < oldStartIdx, 添加新节点
      for (let i = newStartIdx; i <= newEndIdx; i++) {
        mount(nextChildren[i], container, false, oldStartVNode.el)
      }
    } else if (newEndIdx < newStartIdx) {
      // 循环结束后当 newEndIdx < newStartIdx, 移除不存在的元素
      for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        container.removeChild(prevChildren[i].el)
      }
    }
    

    两端比对 DEMO

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { diff... }

    VNode startIndex/endIndex
    a b c d 0 / 3
    d b a c 0 / 3
    next next
    a b c x 0 / 2
    x b a c 1 / 3
    next next
    a b x x 0 / 0
    x b a x 2 / 2
    next next
    a x x x 1 / 0
    x x a x 1 / 0
    next next
    x x x x 0 / 0
    x x x x 0 / 0
    complete complete

    两端比对 - 添加新元素 DEMO

    VNode startIndex/endIndex
    a b 0 / 1
    a d b 0 / 2
    next next
    A b 1 / 1
    A d b 1 / 2
    next next
    A B 1 / 0
    A d B 1 / 1
    next next
    A d B 1 / 0
    A d B 1 / 1
    complete complete

    两端比对 - 添加新元素 DEMO2

    VNode startIndex/endIndex
    a b c 0 / 2
    d a b c 0 / 3
    next next
    a b C 0 / 1
    d a b C 0 / 2
    next next
    a B C 0 / 0
    d a B C 0 / 1
    next next
    A B C 0 / -1
    d A B C 0 / 0
    next next
    d A B C 0 / -1
    d A B C 0 / 0
    next next
    complete complete

    两端比对 - 移除不存在的元素 DEMO

    VNode startIndex/endIndex
    a b c 0 / 2
    a c 0 / 1
    next next
    A b c 1 / 2
    A c 1 / 1
    next next
    A b C 1 / 1
    A C 1 / 0
    next next
    A C 1 / 1
    A C 1 / 0
    next next
    complete complete

    React.js Diff 算法

    updateChildren 大概步骤

    1. 初始化最大索引值变量(lastIndex)为 0
    2. 遍历新节点数组,初始化是否匹配标识符(find)为 false
      1. 循环旧节点数组,查找 key 相同的节点
        1. 存在,先进行 patch,后判断节点是否需要移动(j<lastIndex)
          1. 需要移动=>移动 DOM
          2. 不需要移动=>更新 lastIndex 变量为当前索引,连接新节点到对应 DOM 上
        2. 不存在,则为新节点,需要挂载 DOM 到上一个新节点的后面
      2. 处理已经不存在的节点
        1. 遍历旧节点数组,移除新节点数组中不存在的节点

    updateChildren 代码实现

    // 用来存储寻找过程中遇到的旧节点数组中最大索引值
    let lastIndex = 0;
    // 遍历新的 children
    for (let i = 0; i < nextChildren.length; i++) {
      const nextVNode = nextChildren[i];
      let j = 0,
        find = false;
      // 遍历旧的 children
      for (j; j < prevChildren.length; j++) {
        const prevVNode = prevChildren[j];
        // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
        if (nextVNode.key === prevVNode.key) {
          find = true;
          patch(prevVNode, nextVNode, container);
          // 判断节点是否需要移动
          if (j < lastIndex) {
            // 需要移动
            // refNode 是为了下面调用 insertBefore 函数准备的
            const refNode = nextChildren[i - 1].el.nextSibling;
            // 调用 insertBefor 函数移动 DOM,将当前节点的el 移动到 refNode前
            container.insertBefore(prevVNode.el, refNode);
          } else {
            // 更新 lastIndex
            lastIndex = j;
            // 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
            const el = (nextVNode.el = prevVNode.el);
          }
          break; // 这里需要 break
        }
        // 挂载新节点
        if (!find) {
          // 找到 refNode
          const refNode =
            i - 1 < 0 ? prevChildren[0].el : nextChildren[i - 1].el.nextSibling;
          mount(nextVNode, container, false, refNode);
        }
      }
      // 移除已经不存在的节点
      // 遍历旧的节点
      for (let i = 0; i < prevChildren.length; i++) {
        const prevVNode = prevChildren[i];
        // 拿着旧 VNode 去新 children 中寻找相同的节点
        const has = nextChildren.find(
          (nextVNode) => nextVNode.key === prevVNode.key
        );
        if (!has) {
          // 如果没有找到相同的节点,则移除
          container.removeChild(prevVNode.el);
        }
      }
    }
    

    节点移动、新增、移除 DEMO

    VNode lastIndex 是否需要移动 DOM 是否新增 DOM 是否移除 DOM
    a b d / / / /
    a d c 0 n n n
    next d next next next
    a b d / / / /
    a d c 2 n n n
    next c next next next
    a b d / / / /
    a d c 2 n y n
    next 移除旧 DOM next next next
    a b d c / / / y
    a d c / / / /
    next next next next next
    a b c / / / /
    a d c / / / /
    complete complete complete complete complete

    Reference

    相关文章

      网友评论

        本文标题:[记录]我的Diff算法学习路径

        本文链接:https://www.haomeiwen.com/subject/fsjvbktx.html