# 👉 diff 算法

Vue2 diff 算法是通过同层之间的树节点进行比较,时间复杂度只有 O(n), 算是一个较高效的算法。 本文章主要针对 Vue2 diff 算法展开,最后再简单提及一下与 Vue3 和 React diff 的区别。

# why diff

  • 数据变化,vue 如何更新节点 会先根据真实的 DOM 节点结构生成 virtual DOM,当 virtual DOM 某个节点的数据改变后,会生成一个新虚拟节点 vnode。 触发 set 方法后,消息发布器 Dep 调用 Dep.notify 通知所有订阅者 watcher,订阅者的回调函数 vm.update(vm.render())vm.render()就是生成的新 vnode, vm.update()就会带着新 vnode,和旧虚拟节点 oldVNode 作对比,给真实的 DOM 打补丁,最后更新相应的视图。

  • virtual DOM 和真实 DOM 区别

    使用 document.CreateElement 和 document.CreateTextNode 创建的就是真实节点。

    virtual DOM 是将真实的 DOM 的数据抽取出来,以对象的形式模拟树形结构

    <div id="test" class="div1">
        <p class="p1"></p>
        <p class="p2"></p>
    </div>
    

    对应以下虚拟 DOM 对象

    const vnode = {
        el: div, //对真实的节点的引用,本例中对应document.querySelector('#test.div1')
        tag: "div", //节点的标签
        sel: "div#test.div1", //节点的选择器
        data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
        text: null, //如果是文本节点,对应文本节点的textContent,否则为null
        children: [
            {
                el: p,
                tag: "p",
                sel: "p.p1",
                data: null,
                text: null,
                children: [],
            },
            {
                el: p,
                tag: "p",
                sel: "p.p2",
                data: null,
                text: null,
                children: [],
            },
        ], //存储子节点的数组,每个子节点也是 vnode 结构
    };
    

    virtual DOM 本质是在 JS 操控 DOM 的过程中进行一个缓存。使用真实 DOM 对更新的数据重绘整个视图层是相当消耗性能,通过虚拟 DOM 和 diff 算法得出一些需要修改的最小单位,并对小单位的位图作出对应的更新,这样子可以减少不必要的 DOM 操作,以此来提高性能。

  • diff 比较方式 Vue2 的 diff 通过 patch 函数像打补丁一样修改真实 DOM,比较只会在同层级进行, 不会跨层级比较。(算法核心是同级比较和双端对比)

    比较时,在同级比较中,它会同时从新旧子节点数组的头和尾开始,进行四轮快速比对。如果这四轮都没匹配上,就会利用我们在 v-for 中定义的 key 值去旧节点里查找可复用的节点。

# how diff

1) 通过patch对同级进行比较,然后再往下比较子节点;

首先会通过sameNode判断两个节点的 key、tag、isComment、data 同时定义或不定义以及当标签类型为 input 的时候的 type 相不相同来确定两个节点是不是相同的节点:
如果不是的话就将新节点替换旧节点;
如果是相同节点的话才会进入到 patchVNode 阶段。

2)patchVNode 阶段中,会有几种情况的判断:
a. 判断节点引用是否完全一致,是则直接 return;

b. 然后会对文本节点进行判断,如果是文本节点并且不相等,那么将真实 DOM 对应的文本节点设置为 VNode 的文本节点;

c. 然后再判断一方有子节点和一方没有子节点的情况:
如果旧的一方没有而新一方有子节点,则添加创建并添加新的子节点;
如果旧的一方有而新一方没有子节点,则把旧子节点删除;

d. 最后是判断双方都存在子节点的情况,是则执行 updateChildren 操作。

3)通过 updateChildren 来更新子节点:

在这个阶段核心是采用双端比较的算法,同时从新旧节点的两端进行比较。
在这个过程中,会用到模版编译时的静态标记配合 key 来跳过对比静态节点(没有涉及 data 数据{{}})。

  • 核心策略
    • 同层限制:只比较同一层级的 VNode,不同层级直接销毁重建。
    • 双指针遍历,向中间收缩:对新旧 VNode 列表分别设置oldStartIdx(旧头)、oldEndIdx(旧尾)、newStartIdx(新头)、newEndIdx(新尾)四个指针,优先比较首尾节点以快速复用。
    • key 的作用:当首尾无法匹配时,通过key建立旧节点的索引映射,快速查找可复用节点。

# patch

diff 的过程就是调用 patch 函数,就像打补丁一样修改真实 dom。

patch 函数有两个参数,vnode 和 oldVNode,也就是新旧两个虚拟节点。

function patch(oldVNode, vnode) {
    if (sameVNode(oldVNode, vnode)) {
        patchVNode(oldVNode, vnode);
    } else {
        // 当前oldVNode对应的真实元素节点
        const oEl = oldVNode.el;

        // 父元素
        let parentEle = api.parentNode(oEl);

        // 根据VNode生成新元素
        createEle(vnode);

        if (parentEle !== null) {
            // 将新元素添加进父元素
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl));

            // 移除以前的旧元素节点
            api.removeChild(parentEle, oldVNode.el);
            oldVNode = null;
        }
    }

    // patch最后会返回vnode,vnode和进入patch之前的不同在哪
    return vnode;
}

# sameNode

/*
  判断两个VNode节点是否是同一个节点,需要满足以下条件:
  key相同
  tag相同(当前节点的标签名)
  isComment相同(是否为注释节点)
  是否data都有定义(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)
  当标签是<input>时,type是否相同
*/
function sameVNode(a, b) {
    return (
        a.key === b.key &&
        a.tag === b.tag &&
        a.isComment == b.isComment &&
        isDef(a.data) == isDef(b.data) &&
        sameInputType(a, b)
    );
}

/*
  判断当标签是<input>的时候,type是否相同
  某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型
*/
function sameInputType(a, b) {
    if (a.tag !== "input") return true;

    let i;
    const typeA = isDef((i = a.data)) && isDef((i = i.attrs)) && i.type;
    const typeB = isDef((i = b.data)) && isDef((i = i.attrs)) && i.type;
    return typeA === typeB;
}

# patchNode

当 oldVNode 和 VNode 值得比较后,会对两个节点执行 patchNode:

function patchVNode(oldVNode, vnode) {
    // 引用一致则直接返回
    if (oldVNode === vNode) return;

    // 指向对应的真实DOM
    // vnode表示新节点,此时是没有elm属性的。而在经过createElm方法后,vnode.children中的子节点都有了elm属性,此时只有vnode没有elm属性,而能进到 patchVNode 方法来的新旧节点,一定经过了sameVNode方法的判断,说明他们节点本身几乎一样,所以新节点可以用旧节点的elm
    const el = (vnode.el = oldVNode.el);

    let i;
    let oldCh = oldVNode.children;
    let ch = vnode.children;

    // 如果他们都有文本节点并且不相等,那么将el的文本节点设置为vnode的文本节点
    if (
        oldVNode.text !== null &&
        vnode.text !== null &&
        oldVNode.text !== vnode.text
    ) {
        // node.textContent = vnode.text
        api.setTextContent(el, vnode.text);
    } else {
        updateEle(el, vnode, oldVNode);

        // 如果oldVNode和vnode都有子节点,则执行updateChildren,对子节点进行比较(diff核心)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch);
        }

        // 如果oldVNode没有子节点,而vnode有,则将vode的子节点真实化并新增至el
        else if (ch) {
            createEle(vnode);
        }

        // 如果oldVNode有子节点,而vnode没有,则删除el的子节点
        else if (oldCh) {
            api.removeChildren(el);
        }
    }
}

# updateChildren

function updateChildren(parentElm, oldCh, newCh) {
    // 提取 oldVNode 子节点的头尾Idx以及节点内容,作为移动指针
    let oldStartIdx = 0, oldEndIdx = oldCh.length - 1;
    let oldStartVNode = oldCh[0], oldEndVNode = oldCh[oldEndIdx];

    // 提取 newVNode 子节点的头尾Idx以及节点内容,作为移动指针
    let newStartIdx = 0, newEndIdx = newCh.length - 1;
    let newStartVNode = newCh[0], newEndVNode = newCh[newEndIdx];

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        // 边界情况处理
        if (oldStartVNode == null) {
            oldStartVNode = oldCh[++oldStartIdx];
        } else if (oldEndVNode == null) {
            oldEndVNode = oldCh[--oldEnIdx];
        } else if (newStartVNode == null) {
            newStartVNode = newCh[++newStartVNode];
        } else if (newEndVNode == null) {
            newEndVNode = newCh[--newEndIdx];
        }

        // 前四种情况是在有指定key的时候,分别比较oldCh以及newCh的两头节点 2*2=4 种情况。若判定为同一个VNode则直接patchVNode。

        // 对于 sameVNode(oldStartVNode, newStartVNode) 和 sameVNode(oldEndVNode,newEndVNode) 为 true 的情况,不需要对DOM进行移动
        else if (sameVNode(oldStartVNode, newStartVNode)) {
            // 1. 新旧头匹配:直接复用,指针后移
            patchVNode(oldStartVNode, newStartVNode);

            // 匹配上的两个指针向中间移动
            oldStartVNode = oldCh[++oldStartIdx];
            newStartVNode = newCh[++newStartIdx];
        } else if (sameVNode(oldEndVNode, newEndVNode)) {
           // 2. 新旧尾匹配:直接复用,指针前移
           patchVNode(oldEndVNode, newEndVNode);

            oldEndVNode = oldCh[--oldEndIdx];
            newEndVNode = newCh[--newEndIdx];
        } else if (sameVNode(oldStartVNode, newEndVNode)) {
            // 3. 旧头新尾匹配:复用并移动到尾部
            patchVNode(oldStartVNode, newEndVNode);

            //若oldStartVNode和newEndVNode匹配上了,说明真实DOM的第一个子节点会被移动最后
            api.insertBefore(
                parentElm,
                oldStartVNode.el,
                api.nextSibling(oldEndVNode.el)
            );

            oldStartVNode = oldCh[++oldStartIdx];
            newEndVNode = newCh[--newEndIdx];
        } else if (sameVNode(oldEndVNode, newStartVNode)) {
            // 4. 旧尾新头匹配:复用并移动到头部
            patchVNode(oldEndVNode, newStartVNode);

            // 若oldEndVNode和newStartVNode匹配上了,说明真实DOM的最后子节点会被移动最前
            api.insertBefore(parentElm, oldEndVNode.el, oldStartVNode.el);

            oldEndVNode = oldCh[--oldEndIdx];
            newStartVNode = newCh[++newStartIdx];
        }
        
        // 5. 首尾均不匹配:通过key映射查找可复用节点
        // 根据所有旧子节点的key生成哈希表,key为oldVNode的key,value为对应index序列的哈希表(所有旧子节点的 key 映射到旧节点下标)。
        else {
            // 所有旧子节点的 key 映射到旧节点下标,然后用新vnode的key去找出在旧节点中可以复用的位置。
            // 比如oldCh是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}],oldStartIdx = 0,oldEndIdx = 2
            // 存储所有旧子节点的Key -> index的哈希表,生成 {key0: 0, key1: 1, key2: 2}
           const oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);

            // 新节点匹配旧子节点中的index,即在旧节点中匹配可复用的位置
            const idxInOld = oldKeyToIdx[newStartVNode.key];

            // 无匹配节点则创建新节点,newStartVNode没有key 或 该key没有在老节点中匹配上,则创建一个新的节点插入至oldStartVNode对应位置
            if (!idxInOld) {
                createElm(newStartVNode, parentElm, oldStartVNode.elm);
            } else {
                // 匹配上了,则获取同key的旧子节点,移动到当前位置
                const vnodeToMove = oldCh[idxInOld];

                // 相同key,还要判断和匹配节点是否为sameVNode
                if (vnodeToMove.sel !== newStartVNode.sel) {
                    // 相同key但不是sameVNode的时候,创建新节点并插入至oldStartVNode.el的前边。
                    api.insertBefore(parentElm,createEle(newStartVNode).el,oldStartVNode.el);
                } else {
                    // 相同key且是sameVNode的时候,则将新节点插入,同时往下patchVNode,并把匹配上的旧子节点设置为null
                    pathVNode(vnodeToMove, newStartVNode);
                    oldCh[idxInOld] = null;
                    api.insertBefore(parentElm, vnodeToMove.el, oldStartVNode.el);
                }
            }
            
            newStartVNode = newCh[++newStartIdx];
        }
    }
    
    // 处理剩余节点(新增或删除)
    // 在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较。
    if (oldStartIdx > oldEndIdx) {
        // oldCh先遍历完,则证明剩下的newCh节点是新增的(newStartIdx和newEndIdx之间的vnode),调用addVNodes将其拆入至before后面
        const before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el;
        addVNodes(parentElm, before, newCh, newStartIdx, newEndIdx);
    } else if (newStartIdx > newEndIdx) {
        // newCh先遍历完,则在真实DOM中将[oldStartIdx, oldEndIdx]的多余节点删除
        removeVNodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
}

# Vue3 diff 算法

在 Vue2 中,diff 算法是基于同层比较和双端对比进行的。

在 Vue3 中,diff 算法则是双端对比,前缀 + 后缀 + LIS 优化。

  • 在 key 映射匹配步骤中,引入了**最长递增子序列(LIS)**算法来进一步减少节点移动次数,这个子序列代表的节点是相对位置没有变化的,只需要移动不在这个子序列中的节点即可。
  • 同时也增加了编译时优化
    • 静态提升:在编译阶段将静态节点提升到 render 外,减少运行时对比开销
    • Patch Flags:标记动态节点,比如绑定了数据节点或者属性,精准对比标记过的节点

vue3 diff 的核心策略:

  • 分阶段处理:双端对比:
    • 先匹配前缀相同节点(直接复用,指针后移)。
    • 再匹配后缀相同节点(直接复用,指针前移)。
  • 中间乱序处理:
    • 对剩余节点,用key建立新节点到索引的映射。
    • 生成 “新节点在旧列表中的索引” 数组,计算其 LIS(找出无需移动节点序列,其他节点才移动)。
    • 遍历旧数组,根据 LIS 结果判断是否需要移动节点,不在 LIS 中的节点需要移动到正确位置。(剩余节点根据 LIS 位置移动,减少操作次数。)
// packages/runtime-core/src/renderer.ts
function patchKeyedChildren(c1, c2, container, ...) {
  let i = 0;
  let e1 = c1.length - 1, e2 = c2.length - 1;

  // 1. 处理前缀相同节点
  while (i <= e1 && i <= e2 && isSameVNodeType(c1[i], c2[i])) {
    patch(c1[i], c2[i], container);
    i++;
  }

  // 2. 处理后缀相同节点
  while (i <= e1 && i <= e2 && isSameVNodeType(c1[e1], c2[e2])) {
    patch(c1[e1], c2[e2], container);
    e1--;
    e2--;
  }

  // 3. 旧节点处理完,新增剩余新节点
  if (i > e1) { /* 新增逻辑 */ }
  // 4. 新节点处理完,删除剩余旧节点
  else if (i > e2) { /* 删除逻辑 */ }

  // 5. 处理中间乱序节点
  else {
    const s1 = i, s2 = i;
    
    // 建立新节点key到索引的映射
    const keyToNewIndexMap = new Map();
    for (i = s2; i <= e2; i++) keyToNewIndexMap.set(c2[i].key, i);

    // 遍历旧列表,记录新节点在旧列表中的索引(0表示新增)
    const newIndexToOldIndexMap = new Array(e2 - s2 + 1).fill(0);
    let moved = false, maxNewIndexSoFar = 0;
    for (i = s1; i <= e1; i++) {
      const oldKey = c1[i].key;
      const newIndex = keyToNewIndexMap.get(oldKey);

      if (newIndex !== undefined) {
        newIndexToOldIndexMap[newIndex - s2] = i + 1; // +1避免0
        if (newIndex > maxNewIndexSoFar) maxNewIndexSoFar = newIndex; 
        else moved = true; // 存在需要移动的节点
      } else { 
        /* 删除无匹配的旧节点 */ 
      }
    }

    // 计算LIS(无需移动的节点)
    const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [];
    let lastIndex = increasingNewIndexSequence.length - 1;

    // 从后往前处理新节点,移动或新增
    for (i = e2; i >= s2; i--) {
      const newNode = c2[i];
      const newIndex = i - s2;
      if (newIndexToOldIndexMap[newIndex] === 0) {
        /* 新增节点 */
      } else {
        if (moved && (lastIndex < 0 || newIndex !== increasingNewIndexSequence[lastIndex])) {
          /* 移动节点到正确位置 */
        } else {
          lastIndex--; // LIS节点无需移动
        }
      }
    }
  }
}

# React diff 算法

React 的 diff 基于 Fiber 架构,核心目标是可中断性(时间分片),算法设计更简单,优先保证 UI 响应速度。

因为 Fiber 树是单链表结构,没有子节点数组数据结构,也没有反向指针,也就没有可以供两端同时比较的尾部游标。所以React的这个算法是一个简化的两端比较法,只从头部开始比较。

React diff 采用单向递归比较,它默认从左到右遍历新列表,依靠 key 和节点类型来识别可复用的节点。其算法实现相对简单,可预测性强。

其设计思路:

  • 分层,同层比较:将 DOM 树分为多个层级,然后逐层比较,只比较同一层级的节点,从而减少比较的复杂度。同级比较时按照从左到右的顺序进行比较。
  • 节点类型优先:两个不同类型的元素会生成不同的树,如果元素类型发生了变化,React 会销毁旧树并创建新树;相同类型复用节点,比较属性,然后递归比较子节点;
  • 依赖 Key 属性进行比较:
    • 依赖 key 识别可复用节点(无 key 时按索引比较,可能导致状态错乱)。
    • 从左到右遍历新列表,用 lastPlacedIndex 标记已处理节点的最大旧索引:
      • 若新节点的旧索引 ≤ lastPlacedIndex,则需要移动;否则更新 lastPlacedIndex。

React 的 diff 算法简述:

  • 第一轮遍历 newChildren:处理可复用的节点

    • 从头开始遍历 newChildren ,逐个与 oldFiber 链中的节点进行比较,判断 DOM 节点是否可复用。
    • 如果节点的 key 不同,则不可复用,直接跳出循环,第一轮遍历结束。
    • 如果 key 相同,但是 type 不同,则会重新闯进新 fiber,将 oldFiber 标记为 Deletion 后续删除,并继续遍历。
    • 然后比较 oldFiber.Index 是否小于 lastPlacedIndex 如果是,说明这个节点在新集合中需要向右移动,标记为 Placement ,并更新 lastPlacedIndex;否则则不用移动。
  • 第一轮 newChildren 遍历已完成,新节点已经遍历完成,如果还剩老节点,则将剩下老节点直接删除。

  • 如果老节点没有了,新节点还有 或者 初次渲染,就直接插入。

  • 如果新旧节点都还有,就进行第二次遍历:

    • 把 oldFiber 按照 key 或者 index 放到Map 里,然后遍历新的 Fiber,进行匹配复用。
    • 然后比较 oldFiber.Index 是否小于 lastPlacedIndex 如果是,说明这个节点在新集合中需要向右移动,标记为 Placement ,并更新 lastPlacedIndex;否则则不用移动。
// react-reconciler/src/ReactChildFiber.new.js 简化的核心逻辑

function reconcileChildrenArray(
  returnFiber: Fiber, // currentFirstChild 的父级 fiber 节点
  currentFirstChild: Fiber | null, // 当前执行更新任务的 fiber 节点
  newChildren: Array<*>, // 组件的 render 方法渲染出的新的 ReactElement 节点
  lanes: Lanes,
) {
  
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;
  let oldFiber = currentFirstChild;

  let newIdx = 0;
  let nextOldFiber = null;

  let lastPlacedIndex = 0; // 已处理节点的最大旧索引

  // 初次渲染(false)还是更新(true)
  let shouldTrackSideEffects = !!returnFiber.alternate

  // 1.从头开始遍历 newChildren ,逐个与 oldFiber 链中的节点进行比较,判断 DOM 节点是否可复用
  
  // 第一轮遍历完毕后,会有以下几种情况:
  // 1.1 newChildren 与 oldFiber 同时遍历完👇
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
   if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      // 获取下一个旧节点
      nextOldFiber = oldFiber.sibling;
    }

    // 生成新的节点,判断 key 与 type 是否相同就在 updateSlot 中
    // 只有 key 和 type 都相同才会复用 oldFiber 节点
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes,
    );

    // 1.1.1 key 不同,newFiber 会为 null ,直接跳出循环,第一轮遍历结束
    if (newFiber === null) {
      if (oldFiber === null) { oldFiber = nextOldFiber; }
      break;
    }

    // 1.1.2 key 相同,但是 type 不同的情况,将 oldFiber 打上 Deletion 的标记,循环继续
    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        deleteChild(returnFiber, oldFiber);
      }
    }

    // 1.1.3 记录最后一个可复用的节点在 oldFiber 中的位置索引
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

    // 调整指针
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }

    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
  }
  
  // 1.2 newChildren 遍历完,oldFiber 没遍历完,遍历剩下的 oldFiber 并标记为 Deletion
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  // 1.3 newChildren 没遍历完,oldFiber 遍历完
  // 这说明 newChildren 中剩下的节点都是新插入的节点,只需遍历剩下的 newChildren 创建新的 Fiber 节点并以此标记为 Placement
  if (oldFiber === null) {
    // 遍历剩余的 newChildren
    for (; newIdx < newChildren.length; newIdx++) {
      // 创建新的 Fiber 节点
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) { continue;}
      // 将新的 Fiber 节点标记为 Placement
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 将新的 Fiber 节点用 sibling 指针连接成链表
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    // 返回新的 Fiber 节点组成的链表的头部节点
    return resultingFirstChild;
  }
  

  // 1.4 newChildren 与 oldFiber 都没遍历完,这是 Diff 算法最难的部分。
  // 有可能存在移动了位置的节点,所以为了快速地找到 oldFiber 中可以复用的节点,则创建一个以 oldFiber 的 key 为 key ,oldFiber 为 value 的 Map 数据结构。
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // 遍历剩余的 newChildren ,逐个在 map 中寻找 oldFiber 中可复用的节点。
  // 如果找到可复用的节点,则将 oldIndex 与 lastPlacedIndex 比较。
  // 如果 oldIndex 与 lastPlacedIndex 小,则该节点需要右移,将新的 Fiber 节点标记为 Placement 。否则,将 lastPlacedIndex 更新为 oldIndex 。

  // 遍历 newChildren,在 map 中查找在 oldFiber 中可复用的节点
  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );  

    // 找到了可复用的 Fiber 节点,将其从 map 中删除,因为该节点已经被复用了
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
        if (newFiber.alternate !== null) {
          existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key,);
        }
      }

      // 更新最后一个可复用节点节点的位置索引
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

      // 将 newFiber 用 sibling 连接成单链表
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }

      previousNewFiber = newFiber;
    }
  }
  
  // 遍历完 newChildren 后,还存在 map 在的节点就是剩余的节点,需要标记为删除(Deletion)
  if (shouldTrackSideEffects) {
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }

  // 返回新的 fiber 链表
  return resultingFirstChild;
}

https://juejin.cn/post/7131741751152214030

https://juejin.cn/post/7318446267033452570

Fiber 结构:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;  // 表示节点类型的标记,例如 FunctionComponent 、ClassComponent 等。
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;  // 指向该 FiberNode 的父节点
  this.child = null;  // 指向该 FiberNode 的第一个子节点
  this.sibling = null; // 指向右边第一个兄弟 Fiber 节点
  this.index = 0; // 当前节点在父节点的子节点列表中的索引

  this.ref = null; //存储 FiberNode 的引用信息,与 React 的 Ref 有关

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;

  // 省略一些无关的属性...
}

# 一个例子

旧列表: [A, B, C, D] → 新列表 [A, C, B, E]

  • Vue2 的处理过程

双端指针与四种配对,尝试头头、尾尾、旧头新尾、旧尾新头四种快速命中,最后利用旧节点映射序列进行映射查找,匹配到的旧节点(即可被复用节点)移到OldStart指针前。

步骤1: 使用双端比较,头头比较
A === A ✓ 匹配,两个指针都后移
剩余 [B, C, D] vs [C, B, E]

步骤2: 四种配对都不命中,走映射查找,用新节点去匹配旧节点map,匹配到的旧节点移到OldStart指针前
指   针:oldStart = B (1), newStart = C (1)
比较四项:B≠C, D≠E, B≠E, D≠C
映   射:newStart(C) -> 匹配上了 旧映射 Index 2,
        移动这个旧节点 C 到 oldStart(B) 前面,insertBefore(parent, C.el, B.el)
处理标记:旧数组位置 2 置为占位(如 undefined),newStartIdx++
DOM 变为 [A, C, B, D] 

步骤3:头头再次匹配
指   针: oldStart = B (1), newStart = B (2)
比较四项: B=B
动   作:复用/patch B,指针前移

步骤4:四种配对仍不命中,查映射未找到
指    针:指针推进会跳过旧数组中的占位项(idx=2 处是已移动的 C),oldStart = D (3), newStart = E (3)
比较四项:D≠E、D≠E(尾尾)等
映   射:旧列表中没有 E
动   作:创建新节点 E 并插入到未处理窗口的最前方(以 oldStart(D) 为锚),insertBefore(parent, E.el, D.el)
DOM 暂时变为 [A, C, B, E, D] 。

步骤5:清理剩余旧节点
指   针:newStartIdx++ 越界结束主循环(newStartIdx=4 > newEndIdx=3)
新列表已处理完,旧列表还有 D 未被复用
动作:移除旧节点 D
DOM 最终变为 [A, C, B, E] (与新列表一致)

总结:只需要移动D,其他节点都不动
  • Vue3 的处理过程 先利用双端比较对齐公共前后缀,然后对未知区间构建 key→index 映射,填充 newIndexToOldIndexMap,计算 LIS,最后从右往左遍历新序列执行插入和移动。
步骤1:同步头部公共前缀
  - 相同前缀,无相同后缀,中间乱序:[B,C,D] -> [C, B, E],此时i=1

步骤2:用新未知区间构建映射,然后扫遍历旧列表,记录新节点在旧列表中的索引(0表示新增)
  - 新未知区间 key→index: { C:1, B:2, E:3 }
  - 扫描旧:
    - C 命中新索引 1 ,记录并 patch(C),记录至newIndexToOldIndexMap
    - B 命中新索引 2 ,记录并 patch(B),记录至newIndexToOldIndexMap
    - D 未命中,执行卸载( remove(D) )  
  - 按 新未知区间顺序 得到旧索引序列,newIndexToOldIndexMap 类似 [oldIndex(C)+1, oldIndex(B)+1, E->0] = [3, 2, 0]

步骤3:计算 LIS
  - 新节点 key 映射newIndexToOldIndexMap,即按新未知区间顺序得到旧索引序列(0 基): [C→2, B→1, E→未命中(0)]
  - newIndexToOldIndexMap 类似 [oldIndex(C)+1, oldIndex(B)+1, E->0] = [3, 2, 0]
  - 非零部分 [3, 2] 的 LIS 长度为 1,保留其中一个(常见是保留 B ),意味着 C 需要移动。
  
步骤4:从右向左按新序执行插入/移动
  - j=2,新节点 E 未命中(值为 0),需要创建
    - 锚点 anchor = new[j+1]?.el 不存在,等价于创建追加到末尾
    - insertBefore(parent, E.el, null) → DOM: [A, B, C, E]
  - j=1,新节点 B 在 LIS,跳过,不移动
  - j=0,新节点 C 不在 LIS,需要移动到锚点 B 之前
    - 锚点 anchor = B.el
    - insertBefore(parent, C.el, B.el) → DOM: [A, C, B, E]
  • React 的处理过程 同层比较,key + type 相同则复用;数组新 children 走 O(n) 扫描,不计算 LIS,用 lastPlacedIndex 判定是否移动,小于lastPlacedIndex 则移动。
步骤1:线性遍历新节点列表,逐个与 oldFiber 链中的节点对比
  - 复用/patch A ; lastPlacedIndex = oldIndex(A)=0 。
  - 未知区间:旧 [B, C, D] ,新 [C, B, E] 。 

步骤2: 构建旧节点 key→fiber 映射,遍历新未知区间,通过key筛选可复用节点
  - 旧未知区间 key→fiber:{ B:fiber(B), C:fiber(C), D:fiber(D)}
  - 遍历新未知区间 [C, B, E],检查在旧节点map中是否存在同key节点可以匹配复用
    - C 命中旧节点 C, 旧索引 2 ≥ lastPlacedIndex(0) → 不移动,更新 lastPlacedIndex=2 。
    - B 命中旧节点 B, 旧索引 1 < lastPlacedIndex(2) → 向右移动,更新 lastPlacedIndex=2 。
    - E 未命中,创建并标记 Placement。
  - 新未知区间遍历完,删除旧未知区间中未命中节点 D

步骤3: 提交阶段放置
  - 根据标记执行插入/移动。
  - B 因为需要移动,插入到正确锚点之前(锚点由 getHostSibling 计算,确保最终顺序与新序
  - E 作为新建通常追加到末尾。