React Diff 算法的细节

著有《React18 设计原理》《javascript地月星》等多个专栏。 欢迎关注。

创作不易,内容有帮助记得 ❤️点赞,⭐️收藏 ,🔥评论 ~

本文全部都是原创内容,商业转载请联系作者获得授权,非商业转载需注明出处,感谢理解 ~
推荐指数(值得一读):⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️

原文 👉 juejin.cn/post/763192...

复原、新建、删除

源码包含了 1. updateSlot "查询"然后复用、查不到然后新建 2. placeChild/..标记 flags。nextChildren 变量就是 react ele。

  • 复原:cur.lanes=0,没有任务,wip需要创建一个和cur一样的节点。但是wip flags不标记Placement,因为屏幕上有这个DOM节点。
  • 新增:cur.lanes=1,有任务,进入协调,看看是什么变化,react ele 有 cur 没有,创建新的。 updateSlot 函数返回 wip Fiber。新增也要通过 placeChild 函数给 wip 打上 Placement flags。
  • 删除:虽然 wip 树上本来没有这个 Fiber,但是屏幕上仍然有 ,因此不是 wip 树不创建这个 Fiber 就可以的,还需要从屏幕上删除,通过在父节点帮忙记录,提交的时候才知道删除。

移动 通过 placeChild 给 wip 打上 Placement(新增/移动)标记

出乎意料的,我认为是 "3 往前移动,1,2 没有移动",但是 React 的思路是 "3 没有动,1,2 往后移动"。

注意,红色的箭头其实不应该出现在这里的,红色箭头是提交阶段的内容。我们这里还在协调阶段---------placeChild 给 wip 打上 Placement 标记。

  • 协调阶段:1. 从 react ele 构建 wip Fiber(绿色和黄色节点),2. 打上 Placement(P)。
  • 提交阶段:3.利用 P 标记的节点,计算移动位置。从旧的位置,新的位置,计算 DOM 的移动方式(红色箭头)。

注意,红色箭头描述的是 Fiber 节点对应的 DOM 的移动方式。

例如:✅1 的 DOM 插入到 7 的 DOM 前面。❌而不是 1 插入到 7 前面。我想表达的是:图上的节点都是 Fiber 节点,且位置是正确,按照 react ele 的顺序生成的。不要傻乎乎的去移动 Fiber,要移动的是显示在屏幕上的 DOM。

Q:react ele 是最新正确的,直接用 ele 生成 wip Fiber 就好了,为什么要 reconcile 还要 placeChild?

"查询" cur 与 ele "相同"的 cur,复用能复用的 cur.alternate,最终复用可以复用的 DOM(cur.alternate.stateNode),最终才能减少 DOM 操作。要达到复用 DOM 不得不复用 Fiber。

复用了 wipFiber,但是不知道 wipFiber 怎么移动到这个位置的。placeChild 这里对比 newFiber 和 oldFiber 的 index,打上标记,DOM 才知道 wipFiber 是怎么移动到这个位置的。

placeChild 仅仅给 wip Fiber 打上标记,还没有操作 DOM,真正的操作 DOM 在提交阶段,利用这里计算的 Placements 标记计算 DOM 位置。

Q: 移动怎么知道移动多少位?"这个节点向前移动 2 位,3 位..."似乎没有进行移动位数的计算?

提交阶段不用计算移动多少位。而是知道 insertBefore(A, B) A 插入到 B 的前面。 getHostSibling(A) 找 A 后面最近的,没有 Placement 标记的节点,就是 B。

A 是当前提交遍历的。例如 2 3 都是 A。7 是 B。

Q: 为什么比直接操作 DOM 性能高?

cur 树和 wip 树操作的是同一份 DOM。避免反复新建/删除 DOM。

协调:beginWork 阶段代码(移动和新增,不包含删除)

js 复制代码
function placeChild(newFiber, lastPlacedIndex, newIndex) {
  newFiber.index = newIndex;

  if (!shouldTrackSideEffects) {
    newFiber.flags |= Forked;
    return lastPlacedIndex;
  }

  var current = newFiber.alternate;

  if (current !== null) {
    var oldIndex = current.index;

    if (oldIndex < lastPlacedIndex) {
      // This is a move.
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    } else {
      // This item can stay in place.
      return oldIndex;
    }
  } else {
    // This is an insertion.
    newFiber.flags |= Placement;
    return lastPlacedIndex;
  }
}

提交阶段代码(移动和新增,不包含删除)

js 复制代码
function commitReconciliationEffects(finishedWork) {
  
  var flags = finishedWork.flags;

  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    } 

    finishedWork.flags &= ~Placement;
  }

  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}
function commitPlacement(finishedWork) {
  var parentFiber = getHostParentFiber(finishedWork); 

  switch (parentFiber.tag) {
    case HostComponent://节点类型Fiber
      {
        var parent = parentFiber.stateNode;

        if (parentFiber.flags & ContentReset) {
          resetTextContent(parent);

          parentFiber.flags &= ~ContentReset;
        }
        //getHostSibling(A),找A后面的节点,跳过带有Placement标记的,第一个没有标记的就是要找的。
        //A是当前节点,是一个有Placement标记的节点,代表它是新增的,或者移动的。
        var before = getHostSibling(finishedWork); 
        //insertBefore(A的DOM,B的DOM),操作的是DOM,移动A,B在原位。
        insertOrAppendPlacementNode(finishedWork, before, parent);
        break;
      }

    case HostRoot:
    case HostPortal:
      {
        var _parent = parentFiber.stateNode.containerInfo;

        var _before = getHostSibling(finishedWork);

        insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
        break;
      }

    default:
      throw new Error('Invalid host parent fiber. This error is likely caused by a bug ' + 'in React. Please file an issue.');
  }
}
function getHostSibling(fiber) {

  var node = fiber;

  siblings: while (true) {
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
      
        return null;
      }

      node = node.return;
    }

    node.sibling.return = node.return;
    node = node.sibling;

    while (node.tag !== HostComponent && node.tag !== HostText && node.tag !== DehydratedFragment) {
   
      if (node.flags & Placement) {
        continue siblings;
      } 

      if (node.child === null || node.tag === HostPortal) {
        continue siblings;
      } else {
        node.child.return = node;
        node = node.child;
      }
    } 
    
    if (!(node.flags & Placement)) {
      // ⚠️Found it!
      return node.stateNode;
    }
  }
}
function insertOrAppendPlacementNode(node, before, parent) {
  var tag = node.tag;
  var isHost = tag === HostComponent || tag === HostText;

  if (isHost) {
    var stateNode = node.stateNode;

    if (before) {
      insertBefore(parent, stateNode, before);
    } else {
      appendChild(parent, stateNode);
    }
  } else if (tag === HostPortal) ; else {
    var child = node.child;

    if (child !== null) {
      insertOrAppendPlacementNode(child, before, parent);
      var sibling = child.sibling;

      while (sibling !== null) {
        insertOrAppendPlacementNode(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
} 
function insertBefore(parentInstance, child, beforeChild) {
  parentInstance.insertBefore(child, beforeChild);
}

更新 与 Update Flags

其实删除、移动、新增都属于更新,另一种是"文本颜色等内容更新",这属于简单的 state 变化,newProps!==oldProps 能判断出来不是同一个 react ele。协调里面不需要进一步对比内容是否发生变化,专门标记 Update,因为复用或者新建的 wipFiber 都会同步 react ele 信息,生成最新的 wip Fiber。

相关推荐
Java陈序员8 小时前
轻量运维面板!一款现代化的服务器控制面板工具!
运维·服务器·python·react.js·github
光影少年11 小时前
RN首屏加载速度优化全方案
react native·react.js·掘金·金石计划
前端精髓13 小时前
React 更新 state 中的对象
javascript·react.js·ecmascript
10share19 小时前
我为什么用 React 重写了一个 VitePress
前端·react.js
天道kabuto19 小时前
前端每日知识点:React 与 Vue Diff 算法对比 · 完整总结
vue.js·react.js·前端框架
moMo19 小时前
React 全栈实战:从组件到鉴权的完整指南
react.js
光影少年2 天前
如何实现RN 多环境、多渠道打包
前端·react native·react.js
云上工程笔记2 天前
AstraFlow + React/Vite:如何用自然语言开发并部署一个 AI 咖啡网站原型
前端·人工智能·react.js
光影少年2 天前
React18 对RN 的影响
android·前端·react.js·ios·前端框架