文章目录
- 一文讲清楚React Fiber
- 1. 基础概念
- 1.1浏览器刷新率(帧)
- 1.2 JS执行栈
- 1.3 时间分片
- 1.4 链表
- 2. React Fiber是如何实现更新过程控制
- 2.1 任务拆分
- 2.2挂起、恢复、终止
- 2.2.1 挂起
- 2.2.2 恢复
- 2.2.3 终止
- 2.3 任务具备优先级
一文讲清楚React Fiber
1. 基础概念
1.1浏览器刷新率(帧)
- 页面都是一帧一帧绘制出来的,浏览器大多是60Hz(60帧/s),每一帧耗时16ms左右,每一帧分为以下7个过程
-
- 接手输入事件
-
- 执行回调事件
-
- 开始一帧
-
- 执行RequestAnimationFrame,即RAF
-
- 页面布局,计算样式
-
- 渲染
-
- 执行RequestIdleCallback,即RIC
- 其中,RIC事件并不是每一帧结束都会执行,只有在一帧的16ms内做完了前6件事切还有剩余时间,RIC才会执行。如果执行了RIC事件,那么下一帧就要在事件执行结束后才能继续渲染,所以RIC的执行时间不宜太长,不然浏览器得不到控制权,无法完成下一帧的渲染,会出现页面卡顿
1.2 JS执行栈
- React16之前,是通过原生执行栈递归遍历DOM,会形成一个执行栈,每次更新浏览器会从栈顶开始执行,直到执行栈被清空才会把执行权交给浏览器。而在React中,页面视图都被视为一个个函数执行的结果,这就意味着有多个函数的调用。如果页面很复杂,执行栈就会很深,就要占据很长的一段时间,浏览器渲染就会停滞,就会出现卡顿等问题
1.3 时间分片
- 就是将粒度很小的任务放入一个时间段(一帧)去执行的一种方案,React Fiber就是将多个任务放入一个时间分片去执行
1.4 链表
- 链表的概念不用多少说
- React16之后,使用多向链表代替了原来的树结构,同时还会生成副作用单链表和状态更新单链表
2. React Fiber是如何实现更新过程控制
- 过程可控体现在三方面
-
- 任务拆分
-
- 任务挂起、恢复、终止
-
- 任务具备优先级
2.1 任务拆分
- React Fiber 将遍历VDOM拆分成若干个小任务,每个人物只负责一个节点的处理
2.2挂起、恢复、终止
-
在React Fiber架构中,更新过程的核心在于两棵Fiber树的协同工作:当前工作树(workInProgress)和当前渲染树(current)。这两棵树构成了React实现可中断渲染的基础架构。
-
工作树(workInProgress)是React在执行更新时正在构建的新版本Fiber树。每当应用状态发生变化(如通过setState触发更新),React就会开始构建这棵新树。在构建过程中,每个Fiber节点都- 会记录自身的变更标记(effectTag),最终整棵树会形成完整的变更链表。
-
当前树(current)则代表着上次渲染周期最终呈现的UI对应的Fiber结构。每次更新完成后,新构建的workInProgress树就会成为新的current树。在下一次更新开始时,React会基于这个current树- 创建新的workInProgress树,并通过alternate指针在两树的对应节点间建立关联。
-
在构建新workInProgress树的过程中,React会执行关键的协调算法:
-
通过对比新旧节点(diff算法)来确定需要应用的变更
-
尽可能复用current树中的节点实例,避免不必要的对象创建
-
为每个节点标记具体的更新类型(如新增、修改或删除)
-
整个更新过程本质上就是workInProgress树的渐进式构建过程:
-
React会将构建任务分解为多个工作单元
-
每个工作单元完成后可以暂停让出主线程
-
通过循环调度机制继续处理下一个工作单元
-
这种分片执行方式使得高优先级更新可以中断低优先级任务
-
这种双树机制赋予了React三大核心能力:
-
可中断的渐进式渲染
-
更新优先级的智能调度
-
高效的节点复用策略
-
值得注意的是,所有与任务调度相关的操作(暂停、恢复或取消)都发生在workInProgress树的构建阶段。React通过这种巧妙的架构设计,在保持声明式编程模型的同时,实现了接近原生渲染的性能表现。
2.2.1 挂起
- 当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。
2.2.2 恢复
- 在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。这样完美的解决了调和过程一直占用主线程的问题。
那么问题来了他是如何判断一帧是否有空闲时间的呢?答案就是我们前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。
当恢复执行的时候又是如何知道下一个任务是什么呢?答案在前面提到的链表。在 React Fiber 中每个任务其实就是在处理一个 FiberNode 对象,然后又生成下一个任务需要处理的 FiberNode
class FiberNode {constructor(tag, pendingProps, key, mode) {// 实例属性this.tag = tag; // 标记不同组件类型,如函数组件、类组件、文本、原生组件...this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key ,也就是最终 ReactElement 上的this.elementType = null; // createElement的第一个参数,ReactElement 上的 typethis.type = null; // 表示fiber的真实类型 ,elementType 基本一样,在使用了懒加载之类的功能时可能会不一样this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是RootFiber,那么它上面挂的是 FiberRoot,如果是原生节点就是 dom 对象// fiberthis.return = null; // 父节点,指向上一个 fiberthis.child = null; // 子节点,指向自身下面的第一个 fiberthis.sibling = null; // 兄弟组件, 指向一个兄弟节点this.index = 0; // 一般如果没有兄弟节点的话是0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diffthis.ref = null; // reactElement 上的 ref 属性this.pendingProps = pendingProps; // 新的 propsthis.memoizedProps = null; // 旧的 propsthis.updateQueue = null; // fiber 上的更新队列执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新this.memoizedState = null; // 对应 memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系this.mode = mode; // 表示当前组件下的子组件的渲染方式// effectsthis.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新this.nextEffect = null; // 指向下个需要更新的fiberthis.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成this.childExpirationTime = NoWork; // child 过期时间this.alternate = null; // current 树和 workInprogress 树之间的相互引用}
}
2.2.3 终止
- 其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因
2.3 任务具备优先级
- React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行改任务
- 同时过期时间的大小还代表着任务的优先级。
任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表