不要再说搞不清 React 架构了(上)

1. 前言

最近在温习卡颂老师的《React技术揭秘》,简单地把理念篇和架构篇过了一下,现在梳理成一篇文章分享出来。

2. 快速响应的制约

React 用于构建前端 UI,影响它快速响应的因素主要体现于两个方面:

  • CPU 瓶颈:组件多,JS 操作多,渲染掉帧
  • IO 瓶颈:需要等待网络请求

GUI 渲染线程和 JS 线程互斥,JS 运行和浏览器的布局、绘制无法同时进行。浏览器一般 16 ms 渲染一次,如果 JS 执行时间过长超过 16ms 就没时间去渲染页面,掉帧之后出现了页面卡顿。

2.1 CPU 瓶颈

JS 执行 —> 样式布局 —> 渲染页面

当组件数量过多,意味着 JS 运行时间变长,渲染必然产生卡顿。解决这个问题的思路是:在浏览器每一帧中预留 5ms,React 利用这 5ms 去更新组件,如果不够,就先渲染页面,然后等到下一帧再利用预留时间去执行 JS。

实际上,React 会将长任务分为多个小任务,这被称为时间切片(time slice),它的作用是将同步更新变为可中断的异步更新

当调用 ReactDOM.unstable_createRoot 方法就会开启 Concurrent Mode(并行模式),此时,长任务分为多个 task,每个 5ms 用于 JS 执行,剩余时间用于布局和绘制页面。

2.2 IO 瓶颈

对于网络延迟的感知,解决方案是将人机交互研究的结果整合到真实的 UI 中

  1. 减少过多的中间加载状态:研究表明,屏幕之间切换时显示过多的中间加载状态会让过渡感觉更慢。为了避免频繁且令人不适的更新,Concurrent Mode 会在固定的“时间表”上显示新的加载状态。
  2. 不同交互的优先级:研究显示,像悬停和文本输入这样的交互需要在非常短的时间内处理,而点击和页面过渡可以稍微等待而不会让人感到滞后。Concurrent Mode 内部使用的不同“优先级”大致对应于人类感知研究中的交互类别。
  3. 一体化解决方案:尽管一些专注于用户体验的团队可能会用一次性的解决方案来解决类似的问题,但这些解决方案通常难以长期维护。Concurrent Mode 的目标是将这些 UI 研究成果集成到抽象层中,并提供符合惯用的使用方式。React 作为一个 UI 库,在实现这一目标方面具有良好的定位。

有了解决方案,就要落实在实现上,关键在于:将同步的更新变为可中断的异步更新

3. React 15 架构

React 15 是老版本的架构,之所以被重构,是因为它的渲染逻辑是同步的。

3.1 组成

  • Reconciler(协调器):负责协调更新(找出变化的组件)。它在 render 阶段工作,通过比较新旧 Fiber 树来确定哪些部分需要更新。
  • Renderer(渲染器):负责将更新应用到具体的环境中(如浏览器 DOM 或 React Native),即将变化的组件显示到页面上。它在 commit 阶段工作,执行实际的 DOM 操作或其他平台相关的操作。

3.2 状态更新

状态更新发生在以下几种情况:

  • 路由:url change -> state -> UI
  • 交互状态:interactive -> state -> UI
  • 副作用:state -> effect
  • 状态管理:store change (action) -> state -> UI
  • 缓存:cache invalidate -> state

3.3 Reconciler

当发生更新时,协调器会有如下动作:

  1. 调用组件的 render 方法,将返回的 JSX 转换为 vdom(虚拟 DOM)
  2. 本次 vdom 与上一次的进行对比
  3. 找到本次更新中变化的 vdom
  4. 通知 Renderer 将 vdom 渲染到页面上

3.4 Renderer

每次更新时,都会通知渲染器进行渲染。

一般场景下,这个渲染器就是 ReactDOM,用于浏览器环境。除此之外,还有:

  • ReactNative(渲染 App 原生组件)
  • ReactTest(渲染出纯 JS 对象用于测试)
  • ReactArt(渲染到 Canvas, SVG 或 VML (IE8))

3.5 同步更新

问题在于 Reconciler 中,mount 和 update 的组件都会递归更新子组件,一旦组件层级过深,递归的时间变长,超过 16ms,就会掉帧卡顿。而且在 React 15 架构中,组件发生更新时,它的 Reconciler 和 Renderer 是同步交替执行的。

有以下例子:

```tsx

  • 1
  • 2
  • 3

```

假设 li 的文本内容在点击按钮时发生变化,期望变为:

```tsx

  • 2
  • 3
  • 4

```

它的执行过程是:Reconciler 发现第一个 li 的 1 要变为 2,然后通知 Renderer 去渲染;更新完成后,Reconciler 发现第二个 li 的 2 要变为 3,再通知 Renderer 去渲染;更新完成后,Reconciler 发现第三个 li 的 3 要变为 4,通知 Renderer 去渲染。如此一来,同步交替完成渲染

这就是导致卡顿的关键——整个过程都是同步的。一旦中途中断了更新,比如在渲染了第一个 li 更新后(变为 2),用户在页面上看到的剩下 2 个 li 仍然是原来的 2 和 3。已更新和未更新的内容同时交织在画面上,用户看到的是更新不完全的 DOM

4. React 16 架构

在 React 16 架构中,将同步更新改为了可中断的异步更新。

4.1 组成

与 React 15 的区别在于,多了一个 Scheduler(调度器)。

  • Scheduler(调度器):调度任务优先级,高优先级的任务将会优先进入 Reconciler。
  • Reconciler(协调器)
  • Renderer(渲染器)

4.2 Scheduler

由于以浏览器是否有剩余时间去执行 JS 作为任务中断的标准,这就需要一种机制:有时间就通知我们。React 的 Scheduler 就实现了这种特性,它能在空闲时触发回调的功能,还提供了多种调度优先级供任务设置,是更加完备的 requestIdleCallback polyfill。

4.3 Reconciler

React 15 的协调器是同步递归更新 vdom 的,而 React 16 则是根据 shouldYield 进行可中断的循环过程

tsx /** @noinline */ function workLoopConcurrent() { // Perform work until Scheduler asks us to yield while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } }

Reconciler 一直工作直到 Scheduler 要求停下。什么时候会被中断?

在有其他更高优先级的任务或者当前帧没有剩余时间了,就会被中断。

React 15 中交替更新会出现更新不完全,React 16 解决的方案是——不采用交替工作的方式,而是在 Scheduler 调度完成后将任务发给 Reconciler,Reconciler 会给变化的 vdom 打上标记(Diff 算法):

tsx export const Placement = /* */ 0b0000000000010; export const Update = /* */ 0b0000000000100; export const PlacementAndUpdate = /* */ 0b0000000000110; export const Deletion = /* */ 0b0000000001000;

  • Placement:需要将新的 Fiber 节点插入到树中
  • Update:当前 Fiber 节点需要更新
  • PlacementAndUpdate:当前 Fiber 节点需要插入并且需要更新
  • Deletion:需要删除当前 Fiber 节点

如果不是交替工作,那么现在它们如何工作?

Scheduler 和 Reconciler 会在内存中工作,只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer。由于在渲染之前的调度和协调工作是在内存中进行的,因此不会更新页面上的 DOM,这就解决了更新不完全的问题。

4.4 Renderer

之后,Renderer 根据 Reconciler 为 vdom 打的标记,同步执行对应的 DOM 操作。

5. Fiber Reconciler

Fiber 翻译为纤程,是更加细粒度的程序执行过程。(粗的有:进程、线程、协程)

React 16 提出了 Fiber 架构,这是一种状态更新机制,它支持不同优先级的任务,可中断和恢复,而且恢复后可以使用之前的中间状态。(这就是异步可中断更新的意义)

为什么不用 JS 中的 generator?这是因为它类似于 async 有传染性,会影响上下文函数(其他函数也必须为 async 函数);另一方面,generator 执行的中间状态是连续的不可中断的,需要紧密的上下文关联,无法进行插队。而调度器需要调度任务进行插队。

5.1 Fiber 节点

  • Fiber 节点是 React 内部数据结构的一部分,用于表示组件树中的一个单元。
  • 每个组件都有一个对应的 Fiber 节点,这些节点构成了 React 内部的数据结构。
  • Fiber 节点包含了组件的相关信息,如组件的类型、状态、props,以及与调度和协调相关的信息,如与其他 Fiber 节点的关系、更新优先级等。

```tsx function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode,) { // 作为静态数据结构的属性 this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null;

// 用于连接其他Fiber节点形成Fiber树 this.return = null; this.child = null; this.sibling = null; this.index = 0;

this.ref = null;

// 作为动态的工作单元的属性 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null;

this.mode = mode;

this.effectTag = NoEffect; this.nextEffect = null;

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

// 调度优先级相关 this.lanes = NoLanes; this.childLanes = NoLanes;

// 指向该fiber在另一次更新时对应的fiber this.alternate = null; } ```

5.2 不同架构

在 React 15 中,Reconciler 采用递归的方式执行,数据保存在递归调用栈中,被称为 stake Reconciler,React 16 中,Reconciler 基于 Fiber 节点实现,被称为 Fiber Reconciler

5.3 静态数据结构

作为静态的数据结构来说,每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的 DOM 节点等信息。

5.4 动态工作单元(state 和 effect)

作为动态的工作单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

6. 双缓存机制

在内存中构建并直接替换的技术叫做“双缓存”,这是 React 新架构的工作原理。

通过 Fiber 节点可以组成双缓存 Fiber 树(也是组件树):

  • 在屏幕上显示的 Fiber 树:current Fiber 树
  • 在内存中构建的 Fiber 树:workInProgress Fiber 树

它们之间通过 alternate 熟悉连接:

tsx currentFiber.alternate === workInProgressFiber; workInProgressFiber.alternate === currentFiber;

6.1 Fiber 树结构

假设有以下组件结构:

tsx function App() { return ( <div> i am <span>KaSong</span> </div> ) }

fiber 树结构.png

这和 Fiber 节点的数据结构对应:

tsx // 指向父级Fiber节点 this.return = null; // 指向子Fiber节点 this.child = null; // 指向右边第一个兄弟Fiber节点 this.sibling = null;

6.2 mount 时

mount 时,调用 ReactDOM.render(<App />, document.getElementById('root')) React 会创建一个 fiberRoot 对象,这个对象用于管理整个应用的根节点状态。它的 current 属性指向当前应用的根节点,也就是 <App />,这个根节点被称为 rootFiber

然后根据 JSX 生成 workInProgress Fiber 树(以 RootFiber 为根的 Fiber 树,用于描述组件树的结构和状态)。

在内存中完成构建后就会提交给 Renderer 进行渲染(commit 阶段),在 commit 阶段,React 会执行 DOM 操作来更新实际的 UI,根据 workInProgress 树中的标记执行插入、更新或删除操作。

提交完成后,应用根节点的 current 指针就会指向 workInProgress Fiber 树,此时 workInProgress Fiber 树变为 current Fiber 树,表示页面上显示的 UI 状态。

6.3 update 时

当状态发生更新时,内存中生成 workInProgress 树,构建过程中会复用 current 树的节点数据,然后提交到 Renderer 渲染,渲染完成后,current 指向转换,完成新一轮替换。

**注意:**render 阶段是指在 Reconciler 工作的阶段,workInProgress Fiber 树提交到 Renderer 后,就进入到了 commit 阶段。

7. 总结

影响快速响应的瓶颈来自 CPU 和 IO,React 15 的同步递归交替更新容易产生不完全的 DOM,React 16 的可中断异步更新的 Fiber 架构突破了老架构的困境。Fiber 的三层含义来自不同架构、静态数据结构以及动态工作单元,双缓存 Fiber 树通过内存中的构建和 current 指向切换,搭配调度器巧妙地组合在一起。下篇将介绍协调器中递归构建 Fiber 树的细节。

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值