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。

相关推荐
Rhi6375 小时前
第 2 篇|吐槽向:那些年我们配过的环境,这次终于能跑起来了
react.js·github
wordbaby7 小时前
如何封装一个生产级的 React Native 分页列表 Hook
前端·react native·react.js
M ? A9 小时前
Vue 转 React | VuReact 实时监听开发指南
前端·vue.js·后端·react.js·面试·开源·vureact
openKaka_11 小时前
从 performWorkOnRoot 到 workInProgress tree:React 真正开始 render 的地方
前端·javascript·react.js
nunumaymax11 小时前
【第四章-react ajax】
前端·react.js
爱滑雪的码农11 小时前
React+three.js之场景(Scene),相机(Camera)
前端·javascript·react.js
Lee川12 小时前
登录注册模块的 JWT 认证机制详解
前端·后端·react.js
杨大厨wd1 天前
React 列表页别再手写 useEffect 请求了:从 0 认识 useQuery
react.js