《手写mini React》React diff

112 阅读2分钟
    function cloneFiber(old, new){
        return {
            ...oldFiber,
            ...newFiber
        }
    }
   function Fiber(old, new){
       return {...new }
   }
    function reconcileArray(
        oldArray,
        newArray,
      ){
        // diff 结果存放数组
        const resultArr = [];
        // 最后一个复用的位置
        let lastPlacedIndex = 0;
        // 遍历新数组对象的索引
        let newIdx = 0;
        // 遍历旧数组对象的索引
        let oldIdx = 0;
        
        // 第一轮遍历
        for (; oldIdx < oldArray.length && newIdx < newArray.length; newIdx++, oldIdx++) {
          const oldItem = oldArray[oldIdx];
          const newItem = newArray[newIdx];

          let resultItem = null;
          if(oldItem.key === newItem.key) {
              if(oldItem.type === newItem.type) {
                // key 和 type 都相同  复用
                resultItem = cloneFiber(oldItem, newItem);
              } else {
                // key 相同,type 不相同生成一个新的节点
                resultItem = new Fiber(newItem);
                // 原来的节点删除
                oldItem.flag = 'Deletion';
                resultArr.push(oldItem);
              }
          } else {
            // key 不相同跳出第一轮循环
            break;
          }
          // 更新最后一个复用的位置,并标记对应的副作用
          lastPlacedIndex = placeChild(oldArray, newItem, lastPlacedIndex);
          
          resultArr.push(newItem)
        }

        if (newIdx === newArray.length) {
          // 说明新节点已经遍历完了,需要给剩下的老节点标记删除
          for(; oldIdx < oldArray.length; oldIdx++) {
            const oldItem = oldArray[oldIdx]
            oldItem.flag = 'Deletion';
            resultArr.push(oldItem);
          } 
          // diff结束
          return resultArr;
        }

        if (oldIdx === oldArray.length) {
          // 旧节点已经全部遍历完了,新节点还有,对剩下的新节点标记更新
          for(; newIdx < newArray.length; newIdx++) {
            const newItem = newArray[newIdx]
            newItem.flag = 'Placement';
            resultArr.push(newItem);
          } 
          // diff 结束
          return resultArr;
        }

        // 将剩下的老节点用key值存储到 map 中方便快速查找 
        const existingChildren = mapRemainingChildren(oldArray, oldIdx);

        // 对剩下的新节点进行遍历,并对老节点尽可能的复用
        for (; newIdx < newArray.length; newIdx++) {
          const newItem = newArray[newIdx];
          // 根据 key 获取对应的老节点
          const oldItem = existingChildren.get(newItem.key || newIdx);
          let resultItem = null;
          if(oldItem) {
              // 找到了对应的老节点
            if(oldItem.key === newItem.key) {
                if(oldItem.type === newItem.type) {
                  // key 和 type 都相同  复用
                  resultItem = cloneFiber(oldItem, newItem);
                } else {
                  // key 相同,type 不相同生成一个新的节点
                  resultItem =  new Fiber(newItem);
                  // 原来的节点删除
                  oldItem.flag = 'Deletion';
                  resultArr.push(oldItem);
                }
            } 
            // 将 map 中对应的老节点删除
            existingChildren.delete(newItem.key || newIdx);
          } else{
             // 没找到对应的老节点 生成新节点
             resultItem = {...newItem};
          }
          // 更新最后一个复用的位置,并标记对应的副作用
          lastPlacedIndex = placeChild(oldArray, resultItem, lastPlacedIndex);
          resultArr.push(resultItem);
        }

        // 将 map 中剩下的节点都标记为删除
        existingChildren.forEach(item => {
            item.flag = 'Deletion';
            resultArr.push(item);
        });

        return resultArr;
      }

      function placeChild(oldArray, newItem,lastPlacedIndex) {
        // 这里是替代 react 中取老节点的index
        const oldIdx = oldArray.findIndex(item => newItem.key === item.key);
        if(oldIdx < 0) {
            // 没有找到对应的老节点,说明是新增
            newItem.flag = 'Placement';
        } else {
            // 找到了老节点
            if(oldIdx < lastPlacedIndex) {
                // 说明移动了
                newItem.flag = 'Placement-Move';
                return lastPlacedIndex
            } else {
                // 没有移动,同时更新最后复用的位置
                return oldIdx
            }
        }
      }

      function mapRemainingChildren(oldArr, index) {
        const map = new Map();
        for(let i = index; i < oldArr.length; i++) {
            const item = oldArr[i];
            if(item.key) {
                // key 存在
                map.set(item.key, item);
            } else {
                // key 不存在 用 index 替代 key 值
                map.set(index, item)
            }   
        }
        return map;
      }


  1. cloneFiber 函数:这个函数用于克隆旧的 Fiber 节点并与新的 Fiber 节点进行合并。它将旧 Fiber 节点的属性和状态与新 Fiber 节点的属性和状态进行合并。

  2. Fiber 函数:这个函数用于创建一个新的 Fiber 节点,它将传入的新节点属性对象进行复制。

  3. reconcileArray 函数:这个函数是整个协调算法的核心。它接收两个数组作为参数:oldArray 为旧节点数组,newArray 为新节点数组。函数的主要作用是将新旧节点数组进行对比,并生成 diff 结果数组 resultArr

    • 在第一轮遍历中,它会比较每个节点的 keytype,根据不同情况生成对应的操作:复用、更新或删除。
    • 如果新节点数组已遍历完,但旧节点数组还有剩余节点,它会将剩余的旧节点标记为删除。
    • 如果旧节点数组已遍历完,但新节点数组还有剩余节点,它会将剩余的新节点标记为新增。
  4. placeChild 函数:这个函数根据 key 值在旧节点数组中查找对应的节点,并判断该节点是否发生了移动。它返回最后一个复用的位置,以便在插入新节点时进行参考。

  5. mapRemainingChildren 函数:这个函数用于将剩余的旧节点映射到一个 Map 数据结构中,以便在后续遍历中快速查找并标记删除。

  6. 在主程序中,oldnewArr 分别表示旧节点数组和新节点数组。通过调用 reconcileArray 函数对比两个数组,得到 diff 结果 result,然后输出结果。

参考文章

juejin.cn/post/723787…