一、为什么需要有 Fiber?
在 React v15 版本之前,更新流程都是由递归实现的,一旦更新开始,不可中断,如果某一个更新过大,那么就有可能会给用户造成卡顿感。
为了解决这个问题,React 从 v16 版本开始引入了 Fiber 架构。该架构可以中断更新流程,并且在下一帧宏任务开始,从中断处继续。
二、为什么 Fiber 可以中断?他又该如何继续?
Fiber 架构是由一连串 FiberNode 组合而成的链表。而每一个 FiberNode 对应着一个 React Element,是不是联想到我们说过的 VDOM, 用 JS 描述的真实 DOM,这里便也是类似。
说到链表可能有小伙伴会觉得,链表不是从开头到结尾的吗? 他怎么能中断又继续呢?
常规的链表只有 next 指向下一个节点,但是 Fiber 特殊之处在于,他有 3 个指向:
-
return:指向父节点
-
child:指向第一个子节点
-
sibiling:指向下一个兄弟节点
试想下,我从某个 FiberNode 处中断,但我将这个 FiberNode 保存在全局变量里,等下一次有空,我就可以继续从这个 全局变量 找到这个 FiberNode ,因为有 3 个指向,所以我不担心会丢失任何的信息,只要有一个链表节点,我就可以找到整段链表。这也是 Fiber 可以中断的原因。
那么要如何中断他比较合适,不会让用户感觉卡顿?
我们知道正常显示器来说,一帧大概是 16.6ms ,之前 React v15 会造成卡顿的最大原因就是 JS 遍历这些 VDOM 占用了太多时间,可能都超出了一帧,所以就让用户感觉掉帧卡顿。
现在由于我们更新流程可以中断,那么只需要将这些链表,分成好多个片段,每个帧只执行一个片段的代码 ———— 这就是 Time Slice(时间分片)的思想。
那么 React 是怎么知道这一帧里面有多少时间是可以用来执行片段的?
非常尴尬,这点我要好好说说,我之前查了非常多资料,很多都在说调度器,requestIdelCallback,MessageChannel啥的,但我认为都不是最关键的……后来看了源码才发现,React 就是定义了一个 5ms 的中断时间……
卡颂大佬的这本《React 设计原理》 是有说这个 5ms,只是当初没有看这个,/(ㄒ o ㄒ)/~~ 相见恨晚……
在循环构建/更新 Fiber 时,是从 FiberRoot 开始进行DFS(深度优先)的顺序构造 FiberNode 的。而每进入一个 FiberNode 之前都会去判断,从这一帧的 Fiber更新任务 开始到现在是否经过了 5ms , 5ms 是一个默认的执行时间。一旦超出这个时间,就会中断循环,等到下一帧,从全局变量中取出,即可从中断处继续循环。
【注 1】 大家不要看 5ms 很小,但其实我们一个 FiberNode 是一个 React Element ,5ms 内可以构建好多个 FiberNode
【注 2】 5ms 只是一个大概值,因为每个 FiberNode 大小不一样,比如执行完一连串 FiberNode 后,总耗时为 4.99ms ,因为没超过 5ms ,所以会继续执行向下遍历 FiberNode ,直到总耗时超过 5ms 就中断。
三、双缓存机制
前面说了,Fiber 会每一帧中断,那么在整个 Fiber 树还没构建完时如果展现给用户看,就很不合适。
React 便设计了 2 棵 Fiber 树, 一棵是已经构建好的,给用户看的。另一棵是存储在内存中。
当我们更新 Fiber 时,就去遍历构建内存中的那棵树,等这棵树全部构建完了,我们再展示给用户看即可。
为了实现上述效果,我们需要构建 2 个指针, current 和 workInProgress
-
current:指向用户看到的 FiberNode
-
workInProgress:正在内存中构建的 FiberNode
且这 2 个指针相互之间通过 alternate 属性指向另一棵树中对应的 FiberNode
当我们构建完 workInProgress 时,只要调换 2 个指针指向,将 workInProgress 变为 current 即可(就是将构建完成的当 current,等下一次更新时候,拿另外一棵树去操作,两个指针相互切换)
四、架构迭代
目前有 2 种架构(非Fiber 和 Fiber),4 种策略。
-
旧架构策略(v15 之前的版本)
-
新架构 但未开启并发更新(v16, v17 默认策略)
-
新架构 未开启并发更新,但启用了一些新功能(如: 自动批处理, v18 默认该策略)
-
新架构 开启并发更新 (v18 下使用了并发特性,即开启并发更新)
如下代码,便是在 v18 不使用并发特性 和 使用并发特性 之后的区别,分别对应了 策略3 和 策略4
import { useCallback, useEffect, useState, useTransition } from 'react'
const TestComponent = ({ count }) => {
return (
<>
{new Array(5000).fill(1).map((v, i) => {
return (
<div>
<input type='text' key={i} value={`内容: ${count}`} onChange={() => {}} />
</div>
)
})}
</>
)
}
const Test1 = () => {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('hello')
const updateCount = () => {
setMessage((message) => message + '_hello')
setCount((count) => count + 1)
}
return (
<>
<h1>不开启并发特性</h1>
<button onClick={updateCount}>Count: {count}</button>
<div>{message}</div>
<TestComponent count={count} />
</>
)
}
const Test2 = () => {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('hello')
const [isPending, startTransition] = useTransition()
const updateCount = useCallback(() => {
startTransition(() => {
// 并发运行这个更新操作
setCount((count) => count + 1)
})
}, [])
useEffect(() => {
if (isPending) {
console.log('6')
setMessage((message) => message + '_hello')
}
}, [isPending])
return (
<>
<h1>开启并发特性</h1>
<button onClick={updateCount}>Count: {count}</button>
<div>{message}</div>
<div>{isPending ? 'Loading...' : <TestComponent count={count} />}</div>
</>
)
}
export default Test2