Fiber Reconciler 简单介绍

发布于 2023-05-04 20:13:09 字数 6588 浏览 61 评论 0

在 React 官方文档中,对 Reconciler 的定义是两个 DOM 树 diff 过程的算法。当用户界面第一次渲染界面时,React 会创建两颗树,一颗是节点树,每个节点是 React 元素。同时创建一颗虚拟树 virtualDOM,是渲染 DOM 树的副本,在界面更新之前,React 会递归比较两棵树每一个节点,得出变更结论并进行界面渲染。

React 16 版本之前,内部是利用 Stack Reconciler 去递归并创建虚拟 DOM,diff 过程无法中断,如果组件树层级过深,同时运行其他任务操作的时候,主线程会被阻塞,此刻如果界面有布局改变、交互动画等等渲染,用户就会察觉到卡顿。于是 React 16 采用了新的 Fiber Reconciler 进行增量渲染。

Fiber 架构中引入了新的数据结构 Fiber Node,这是一个单链表结构,Fiber Node Tree 根据 React Element Tree 生成,并用来驱动真实 DOM 的渲染。

调度器会将 diff 递归任务拆分成 chunk 块,每个任务都有优先级标记,这样即使线程中有一个长时间执行的渲染任务,也可以暂停终止,并会执行其他优先级更高的任务,这个过程称为 Scheduling 调度。优先级高的任务完成后,再继续恢复原来的任务执行。

由于 Fiber 调度方式可使任务暂停、终止和复用的特殊性,组件渲染有会被中断的情况,一旦生命周期被中断后,render 以及之前生命周期函数会被再次调用,所以 shouldComponentUpdate 不应该写 side effects 代码,例如修改全局变量、传参、DOM,发起请求等等。

当 ReactDOM.render 启动或调用 setState() 的时候开始创建/更新 fiber tree,并走初始渲染/更新渲染流程。

初始渲染

以下描述,大写 F 的 Fiber 指的是协调器本身,小写 f 的 fiber 指的是一个工作单位(the basic unit of work)。

初始渲染,Fiber 会创建一个根 fiber 节点名为 HostRoot,然后从根 React 元素开始遍历,并为其创建一个 fiber 节点,接着向下找到子节点,为子元素创建 fiber 节点。持续下去直到最后一个子元素,然后检查子元素是否有兄弟节点,如果有就遍历兄弟元素,再到兄弟元素的子元素,如果没有兄弟节点就会返回到父节点。

假设你的 JSX 代码是这样的:

function App() { 
  return (
    <div className="wrapper"> 
      <div className="list"> 
        <span className="content">content 1</span> 
        <span className="content">content 2</span> 
      </div>
      <div className="btn">
        <button className="add">Add</button>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));  // HostRoot

那么最终的 fiber tree 就是下图的链表结构:

fiber-tree

多个 fiber 节点通过自身类型定义里 return、child、sibling 的指向,形成链表关系,并最终生成 current tree。fiber 节点的结构和详细说明可以在 React 源码中 type 类型定义 找到。

这是对初始渲染的简单描述,在了解更新渲染之前,首先要了解 requestAnimationFrame。

requestAnimationFrame(rAF)

在 Fiber 中,rAF 负责优先级高的函数在下一个动画帧(before the next animation frame)之前调用。

当我们在调用 window.requestAnimationFrame() 是为了让浏览器执行一个动画,并且要求浏览器在下次重绘之前,调用传入的回调函数去更新动画,回调函数会在浏览器下一次重绘之前执行。

如果屏幕刷新率为 60Hz,那么每执行一次 rAF,一帧的时间则是 16.6ms。每一帧的生命周期里,都包含了 Input Events 用户交互行为、JS 执行、rAF 调用,Layout 布局计算以及 Paint 重绘。

frame-lifecycle

从生命周期可得知,rAF 的回调时机会在 Layout 布局计算之前调用。

由于用户切到其他标签页的情况下 rAF 会暂停调用,所以 Fiber 做了一个 pollyfill,如果页面失焦则 setTimeout 会替代 rAF 的工作,页面聚焦后再通过 rAF 取消 setTimeout。

被弃用的 requestIdleCallback(rIC)

在未被弃用前,rIC 主要是负责优先级低或者非必要的函数任务调用,如果一帧在 16ms 内已经执行完了当前任务,在帧结束时之前的空闲时间则会调用优先级低的任务。

以下 rIC 的用法,在代码段中 lowPriorityWork 是一个回调函数,将在帧结束时的空闲时间内被调用。

function lowPriorityWork(deadline) {
  while (deadline.timeRemaining() > 0 && workList.length > 0)
    performUnitOfWork();

  if (workList.length > 0) requestIdleCallback(lowPriorityWork);
}

当 lowPriorityWork 被调用时候,会传入一个 deadline 对象,对象的 timeRemaining 函数返回最近的剩余空闲时间,如果大于 0 则执行 performUnitOfWork 去做一些优先级更高的工作。如果没有空闲时间则在下一帧继续调用 lowPriorityWork。

被弃用的原因,React 核心开发者 Dan 解释 因为 rIC 触发过晚导致浪费 CPU 时间,所以改用了 5ms 一次性的循环。我在网上查阅相关资料发现一个 github issues 其中提到 rIC 一秒只会调用 20 次,这远远低于 rAF 的调用次数。

requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work. —— from Releasing Suspense

更新渲染

回到更新,对于每次更新,Fiber 都会创建 workInProgress tree,这就是 diff 的过程。与初始渲染不同,Fiber 不会再次为每个元素创建新的 fiber 节点,而是根据 current tree 建立 workInProgress tree。Fiber 从 current tree 根部开始遍历,处理每个 fiber。

current_workInProgress

fiber 工作单位的分配优先级,以及对 fiber 进行暂停、终止和复用是由 Scheduler 模块负责的,以下是所有优先级的定义:

  • NoWork 0 没有待处理的工作
  • SynchronousPriority 1 文本输入
  • TaskPriority 2 需要在当前调度结束时完成
  • AnimationPriority 3 需要在下一帧之前完成
  • HighPriority 4 需要尽快完成的交互
  • LowPriority 5 数据更新
  • OffscreenPriority 6 不会显示但以防将来会显示的任务

目前是可中断的协调阶段(reconciliation phase),协调阶段的结果除了会产生 workInProgress tree 外还会产生一个 Effect List。effect 指的是变更操作,像是对宿主组件进行插入、更新或者删除,调用类组件节点的生命周期方法。这些 Fiber Node 会被标记上一个 effect tag。

协调阶段对于用户来说是不可见的,这些都是 Fiber 背后的工作。这个阶段结束后,Fiber 就会准备提交更新,进入到下一个用户可见且不可中断的阶段:提交阶段(commit phase)。

在提交阶段,workInProgress tree 会成为 current tree,React 遍历 Effect List 并一次性更新到 DOM 上。以上就是虚拟树 diff 过程中对 Fiber Reconciler 的基本阐述。

总结

在最开始的时候,React 会创建一棵初始的Fiber树,作为current树。这个初始的Fiber树通常是通过调用ReactDOM.render()方法生成的,并且它对应的是整个应用的初始状态。

当组件进行更新时,React 会创建一个新的Fiber树,命名为workInProgress树。这个workInProgress树是一个全新的Fiber树,与current树无关,用于计算出新的虚拟DOM树。在reconciliation 过程中,React 会在 workInProgress 树中构建新的Fiber节点,并根据当前节点的状态计算出需要渲染的内容和对应的DOM节点。

当计算完成后,React 会通过 diff 算法比较 workInProgress 树和 current 树的差异,找出需要更新的部分,然后将这些更新应用到页面上。如果更新成功,React会将workInProgress树赋值给current树,这样就完成了一次更新。此时,current 树就是最新的 Fiber 树,代表了应用的最新状态。

在这个过程中,没有涉及到 alternate 备份树。alternate 树是在 workInProgress 树计算完成后,用于备份current树的一棵Fiber树,用于恢复当前的渲染状态。如果在更新过程中出现了错误,React 会使用这个alternate树来回滚到之前的渲染状态,并重试更新。因此,alternate树主要是用于保证React在更新过程中的安全性和健壮性。

参考:An Introduction to React Fiber - The Algorithm Behind React

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

挽清梦

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

qq_eQNo9e

文章 0 评论 0

内心旳酸楚

文章 0 评论 0

mb_BlPo2I8v

文章 0 评论 0

alipaysp_ZRaVhH1Dn

文章 0 评论 0

alipaysp_VP2a8Q4rgx

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文