这是我翻译的
React Fiber Architecture
介绍
React Fiber 是对 React 核心算法的重新实现
它的目的是让渲染更加流畅
Fiber 最重要的特性是可以 增量渲染(incremental rendering):
- 可以把渲染事务(rendering work)拆分成多个块(chunk),并且把这些 chunk 分布到多个帧(frame)去完成
其他关键特性包括:
- 有新的更新时,停止、取消、或者重用 work 的功能
- 为不同类型的更新设置优先级
- and new concurrency primitives
必要的知识
强烈建议在继续阅读本文之前学习以下前置知识:
- React Components, Elements, and Instances - Components 是经常被提到的术语,掌握这些术语非常重要
- Reconciliation - 一个对 React 的 reconciliation 算法的 high-level 的阐述
- React - Basic Theoretical Concepts - React 概念模型的一个大概描述。如果是第一次阅读,可能有一些地方不会理解,不过没关系,随着时间的推移,你会慢慢了解
- Design Principles - 请特别注意 scheduling 部分。它很好的解释了为什么用 React Fiber
reconciliation
React API 的中心思想是,让开发者只关注数据的显示,和数据状态的处理逻辑,而不用关注如何有效的把数据从一个状态变为另一个状态
一个 app 每次更改都需要重新渲染,这是非常消耗性能的,为此,React 做了很多优化,这些优化当中,大部分属于 reconciliation 的过程
现在来说下 reconciliation
reconciliation 是协调算法的意思。通常理解为 “virtual DOM” 背后的算法,大概是这样:
当一个 React 应用程序渲染的时候,会生成一个用来描述当前 app 的一个树形结构,这个“树”保存在内存中。然后这个“树”会在渲染环境刷新 - 举个例子:
在浏览器环境中,reconcilier 表示为一些 DOM 操作,当 app 被更新的时候(通常通过setState
)会生成一个新的“树”。这个新的“树”会被拿去和之前的“树”做比较来确定需要哪些操作来更新当前的 APP
尽管 Fiber 彻底重写了 reconciler(协调器),但是主要思想是一样的,最重要的两点是:
- 不同类型的组件会生成不同的树,React 将不会比较它们,而是直接用新的完全替代旧的
- 列表的区分是使用 key 作为标识,key 应该是稳定,可预测,并且是唯一的
reconciliation VS rendering
DOM 仅仅是 React 可以渲染的环境之一,另外,React Native 也可以在 native iOS 和 Android views 环境渲染(这也是为什么我们常说的 “virtual DOM” 有点用词不当的原因)
React 支持在不同的宿主环境渲染是因为:React 把 reconciliation 和 rendering 设计成两个独立的阶段
- reconciler 的工作是计算出“树”中的哪些部分已经发生了改变
- 然后,renderer 通过 reconciler 计算出来的更新信息,将更新应用到 app 上
React 这种将 reconciliation 和 renderer 分开的设计,意味着,React DOM 和 React Native 可以使用他们自己的 renderer,共享 React core 提供的相同的 reconciler
Fiber 重写了 reconciler,虽然 rendering 做了一些更改来适应新的架构,但是 Fiber 主要的关注点是 reconciler
Scheduling(调度)
scheduling: 这是个进程,这个进程可以决定在什么时间点执行什么 work
work: 指需要执行的计算,work 通常是 update 的结果(e.g. setState
)
React 的 Design Principles 对这方面做了详细的解释:
In its current implementation React walks the tree recursively and calls render functions of the whole updated tree during a single tick. However in the future it might start delaying some updates to avoid dropping frames.
This is a common theme in React design. Some popular libraries implement the "push" approach where computations are performed when the new data is available. React, however, sticks to the "pull" approach where computations can be delayed until necessary.
React is not a generic data processing library. It is a library for building user interfaces. We think that it is uniquely positioned in an app to know which computations are relevant right now and which are not.
If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.
This is a common theme in React design. Some popular libraries implement the "push" approach where computations are performed when the new data is available. React, however, sticks to the "pull" approach where computations can be delayed until necessary.
React is not a generic data processing library. It is a library for building user interfaces. We think that it is uniquely positioned in an app to know which computations are relevant right now and which are not.
If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.
关键点是:
- 在 UI 层面,没有必要每个更新都需要立刻被执行,如果每个更新都被执行,会导致丢帧,降低用户体验
- 不同类型的更新有不同的优先级 - 动画的更新需要比数据的更新更快的完成
- 一个 push-based 方法要求程序员去决定如何调度 work。一个 pull-based 方法允许框架(React)去决定如何调度 work
目前 React 并没有充分利用 scheduling,在一个 sub-tree 中,一个更新的结果会导致整个 sub-tree 重新 render
而改造 React 的核心算法,合理的利用 scheduling 是 Fiber 的宗旨
什么是 fiber
现在从技术层面介绍 React Fiber 架构
Fiber 主要的目的是让 React 能够更好的利用 scheduling,为此,我们需要它:
- 可以停止 work,并且可以返回到停止的 work
- 可以给 work 制定优先级
- 可以复用之前已经完成的 work
- 中断不需要的 work
为了做到这些,首先我们需要把 work 拆分成 unit。在某种意义上来说,unit 就是 fiber。一个 fiber 就代表一个 unit 的 work
为了了解的更详细一点,让我们回到 React components as functions of data,通常表示为
v = f(d)
它表示,渲染 React 应用程序类似于调用一个函数,该函数的主体包含对其他函数的调用。以此类推,这个类比在我们思考 fiber 的时候很有帮助
计算机通常使用 call stack 来追踪程序的执行。当一个函数被执行的时候,一个新的 stack frame 被添加进 stack。这个 stack frame 代表这个函数正在执行的 work
当处理 UI 的时候,不能一次性处理太多的工作,如果处理太多会出现掉帧的情况。另外,如果更新的比较频繁的话,有一些 work 是不必要的,这就是 UI 组件和函数之间不同的地方,因为组件比一般的函数具有更多的特定关注点
比较新的浏览器(和 React Native)实现了一些 API 来解决这些问题:
requestIdleCallback
会安排低优先级的任务在空闲的时候被调用,requestAnimationFrame
会在下一帧动画上调用高优先级的任务。问题是,如果你使用这些 API,你需要一个方法将渲染的 work 分解成 incremental units(增量单元)。如果你只遵循调用栈的操作方式,它会一直工作直到栈为空
React Fiber 的目的有两个:
- 自定义调用堆栈来优化渲染
- 随意中断调用堆栈,并且手动操作
Fiber 是专门为 React 组件实现的堆栈重构。你可以把单个 fiber 当作一个 虚拟的堆栈帧(virtual stack frame)
把堆栈重新实现一遍的好处是:
- 可以把堆 virtual stack frame 保存在内存中,然后执行它们(任何时候)
- 手动处理堆栈帧还可以释放并发和错误边界等功能
fiber 的结构
具体来说,fiber 是一个 javascript 对象
也就是说,一个 fiber 对应了一个 virtual stack frame,同时也对应了一个组件的实例
下面是 fiber 的一些重要字段
type
和 key
Fiber 的 type
和 key
的作用和 React 元素一样。(实际上,一个 fiber 从组件创建时,这两个字段会直接复制过来)
fiber 的 type
描述了它对应的组件
- 对于复合组件,type 是 function 或者 class 本身。
- 对于原生组件(div,span 等),type 为字符串
从理论上来说,type
是执行时被堆栈跟踪的函数
除了 type
以外,key
是在 reconciliation 中决定 fiber 是否可以被重用
child
和 sibling
这些字段对应其他的 fiber,描述 fiber 树的递归结构
child fiber 代表组件的 render()
方法返回回来的值。比如在下面的例子中 Parent
的 child fiber 就是 Child
function Parent() {
return <Child />
}
sibiling fiber 描述 render()
方法返回多个节点的情况(这是 Fiber 中的新特性):
function Parent() {
return [<Child />, <Child />]
}
在这个例子中,child fibers 是一个 head 指针式第一个 child 的单链表
Parent 的第一个 child 是 Child1,Child1 的 sibling 是 Child2
你可以吧 child fiber 想象成一个尾递归
return
当前的 fiber 执行完之后会 return 一个 fiber
从概念上来说,和返回的 stack frame 的地址是一样的
return 的 fiber 也可以看作是 parent fiber
如果一个 fiber 有很多 child fibers,每一个 child fiber 返回的 fiber 都可以看作是 它的 parent fiber
所以在上个例子中 Child1 和 Child2 的 return fiber 都是 Parent
pendingProps
和 memoizedProps
一般来说, props 代表一个 function 的参数,一个 fiber 的 pendingProps
在执行开始的时候设置,memoizedProps
在结束的时候设置
当传入的 pendingProps
等于 memoizedProps
的时候,它表示可以重用 fiber 的前一个输出,避免不必要的工作
pendingWorkPriority
这是个数字,代表 fiber 工作的优先级。ReactPriorityLevel 这个 module 列出了不同的优先级和他们代表什么
0 表示 NoWork
,一个很大的数字代表一个低的优先级。举个例子,你可以使用下面这个函数来检查一个 fiber 的优先级是否比给出来的 level 高
function matchesPriority(fiber, priority) {
return fiber.pendingWorkPriority !== 0 &&
fiber.pendingWorkPriority <= priority
}
上面这段代码不属于 Fiber 的源码
scheduler 通过查找优先级的方式来决定下一个要执行的 work unit
alternate
flush
- flush fiber 是将其输出到屏幕上
work-in-process
- 还没有完成的 fiber。通常来说表示一个没有返回的 stack frame
在任何时候一个组件实例都有两个与它对应的 fiber,current fiber 和 work-in-process fiber
current fiber 和 work-in-process fiber 是交替进行的