浅谈React-Fiber

15次阅读

共计 6830 个字符,预计需要花费 18 分钟才能阅读完成。

背景

前段时间准备前端招聘事项,复习前端 React 相关知识;复习 React16 新的生命周期:弃用了 componentWillMountcomponentWillReceivePorpscomponentWillUpdate 三个生命周期,新增了 getDerivedStateFromPropsgetSnapshotBeforeUpdate 来代替弃用的三个钩子函数。
发现 React 生命周期的文章很少说到 React 官方为什么要弃用这三生命周期的原因,查阅相关资料了解到根本原因是 V16 版本重构核心算法架构:React Fiber;查阅资料过程中对 React Fiber 有了一定了解,本文就相关资料整理出个人对 Fiber 的理解,与大家一起简单认识下 React Fiber

React Fiber 是什么?

官方的一句话解释是“React Fiber 是对核心算法的一次重新实现”。Fiber 架构调整很早就官宣了,但官方经过两年时间才在 V16 版本正式发布。官方概念解释太笼统,其实简单来说 React Fiber 是一个新的任务调和器(Reconciliation),本文后续将详细解释。

为什么叫“Fiber”?

大家应该都清楚 进程 (Process)和 线程 (Thread)的概念, 进程 是操作系统分配资源的最小单元,线程 是操作系统调度的最小单元,在计算机科学中还有一个概念叫做 Fiber,英文含义就是“纤维”,意指比 Thread 更细的线,也就是比线程 (Thread) 控制得更精密的并发处理机制。
上面说的 Fiber 和 React Fiber 不是相同的概念,但是,React 团队把这个功能命名为 Fiber,含义也是更加紧密的处理机制,比 Thread 更细。

Fiber 架构解决了什么问题?

为什么官方要花 2 年多的时间来重构 React 核心算法?
首先要从 Fiber 算法架构前 React 存在的问题说起!说起 React 算法架构避不开“Reconciliaton”。

Reconciliation

React 官方核心算法名称是 Reconciliation,中文翻译是“协调”!React diff 算法的实现 就与之相关。
先简单回顾下 React Diff: React 首创了“虚拟 DOM”概念,“虚拟 DOM”能火并流行起来主要原因在于该概念对前端性能优化的突破性创新;
稍微了解浏览器加载页面原理的前端同学都知道网页性能问题大都出现在 DOM 节点频繁操作上;
而 React 通过“虚拟 DOM”+ React Diff 算法保证了前端性能;

传统 Diff 算法

通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3),n 是树的节点数,这个有多可怕呢?——如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也 很难在一秒内 计算出差异。

React Diff 算法

将 Virtual DOM 树转换成 actual DOM 树的最少操作的过程 称为 协调(Reconciliaton)。
React Diff 三大策略:
1.tree diff;
2.component diff;
3.element diff;
PS: 之前 H5 开发遇到的 State 中变量更新但视图未更新的 Bug 就是 element diff 检测导致。解决方案:1. 两种业务场景下的 DOM 节点尽量避免雷同;2. 两种业务场景下的 DOM 节点样式避免雷同;

在 V16 版本之前 协调机制Stack reconciler,V16 版本发布 Fiber 架构后是 Fiber reconciler

Stack reconciler

Stack reconciler 源码

// React V15: react/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
/**
 * ------------------ The Life-Cycle of a Composite Component ------------------
 *
 * - constructor: Initialization of state. The instance is now retained.
 *   - componentWillMount
 *   - render
 *   - [children's constructors]          // 子组件 constructor()
 *     - [children's componentWillMount and render]   // 子组件 willmount render
 *     - [children's componentDidMount]  // 子组件先于父组件完成挂载 didmount
 *     - componentDidMount
 *
 *       Update Phases:
 *       - componentWillReceiveProps (only called if parent updated)
 *       - shouldComponentUpdate
 *         - componentWillUpdate
 *           - render
 *           - [children's constructors or receive props phases]
 *         - componentDidUpdate
 *
 *     - componentWillUnmount
 *     - [children's componentWillUnmount]
 *   - [children destroyed]
 * - (destroyed): The instance is now blank, released by React and ready for GC.
 *
 * -----------------------------------------------------------------------------

Stack reconciler 存在的问题

Stack reconciler 的工作流程很像函数的调用过程。父组件里调子组件,可以类比为函数的递归(这也是为什么被称为 stack reconciler 的原因)。
在 setState 后,react 会立即开始 reconciliation 过程,从父节点(Virtual DOM)开始遍历,以找出不同。将所有的 Virtual DOM 遍历完成后,reconciler 才能给出当前需要修改真实 DOM 的信息,并传递给 renderer,进行渲染,然后屏幕上才会显示此次更新内容。
对于特别庞大的 DOM 树来说,reconciliation 过程会很长(x00ms),在这期间,主线程是被 js 占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。

网友测试使用 React V15,当 DOM 节点数量达到 100000 时,加载页面时间竟然要 7 秒;详情
当然以上极端情况一般不会出现,官方为了解决这种特殊情况。在 Fiber 架构中使用了 Fiber reconciler。

Fiber reconciler

原来的 React 更新任务是采用递归形式,那么现在如果任务想中断,在递归中是很难处理,所以 React 改成了大循环模式,修改了生命周期也是因为任务可中断

Fiber reconciler 源码

React 的相关代码都放在 packages 文件夹里。(PS:源码一直在更新,以下路径有时效性不一定准确)

├── packages --------------------- React 实现的相关代码
│   ├── create-subscription ------ 在组件里订阅额外数据的工具
│   ├── events ------------------- React 事件相关
│   ├── react -------------------- 组件与虚拟 DOM 模型
│   ├── react-art ---------------- 画图相关库
│   ├── react-dom ---------------- ReactDom
│   ├── react-native-renderer ---- ReactNative
│   ├── react-reconciler --------- React 调制器
│   ├── react-scheduler ---------- 规划 React 初始化,更新等等
│   ├── react-test-renderer ------ 实验性的 React 渲染器
│   ├── shared ------------------- 公共代码
│   ├── simple-cache-provider ---- 为 React 应用提供缓存

这里面我们主要关注 reconciler 这个模块,packages/react-reconciler/src

├── react-reconciler ------------------------ reconciler 相关代码
│   ├── ReactFiberReconciler.js ------------- 模块入口
├─ Model ----------------------------------------
│   ├── ReactFiber.js ----------------------- Fiber 相关
│   ├── ReactUpdateQueue.js ----------------- state 操作队列
│   ├── ReactFiberRoot.js ------------------- RootFiber 相关
├─ Flow -----------------------------------------
│   ├── ReactFiberScheduler.js -------------- 1. 总体调度系统
│   ├── ReactFiberBeginWork.js -------------- 2.Fiber 解析调度
│   ├── ReactFiberCompleteWork.js ----------- 3. 创建 DOM 
│   ├── ReactFiberCommitWork.js ------------- 4.DOM 布局
├─ Assist ---------------------------------------
│   ├── ReactChildFiber.js ------------------ children 转换成 subFiber
│   ├── ReactFiberTreeReflection.js --------- 检索 Fiber
│   ├── ReactFiberClassComponent.js --------- 组件生命周期
│   ├── stateReactFiberExpirationTime.js ---- 调度器优先级
│   ├── ReactTypeOfMode.js ------------------ Fiber mode type
│   ├── ReactFiberHostConfig.js ------------- 调度器调用渲染器入口

Fiber reconciler 优化思路

Fiber reconciler 使用了 scheduling(调度)这一过程,每次只做一个很小的任务,做完后能够“喘口气儿”,回到主线程看下有没有什么更高优先级的任务需要处理,如果有则先处理更高优先级的任务,没有则继续执行(cooperative scheduling 合作式调度)。

网友测试使用 React V16,当 DOM 节点数量达到 100000 时,页面能正常加载,输入交互也正常了;详情

所以 Fiber 架构就是用 异步的方式解决旧版本 同步递归导致的性能问题。

Fiber 核心算法

编程最重要的是思想而不是代码,本段主要理清 Fiber 架构内核算法的编码思路;

Fiber 源码解析

之前一个师弟问我关于 Fiber 的小问题:
Fiber 框架是否会自动给 Fiber Node 打上优先级?
如果给 Fiber Node 打上的是 async,是否会给给它设置 expirationTime
带着以上问题看源码,结论:
框架给每个 Fiber Node 打上优先级(nowork, sync, async), 不管是 sync 还是 async 都会给 该 Fiber Node 设置 expirationTime,expirationTime 越小优先级越高。

个人阅读源码细节就不放了,因为发现网上有更系统的 Fiber 源码文章,虽然官方源码已更新至 Flow 语法,但算法并没太大改变:
React Fiber 源码分析(介绍)
React Fiber 源码分析 第一篇
React Fiber 源码分析 第二篇(同步模式)
React Fiber 源码分析 第三篇(异步状态)

优先级

module.exports = {
  NoWork: 0, // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  AnimationPriority: 2, // Needs to complete before the next frame.
  HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4, // Data fetching, or result from updating stores.
  OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};

React Fiber 每个工作单元运行时有 6 种优先级:
synchronous 与之前的 Stack reconciler 操作一样,同步执行
task 在 next tick 之前执行
animation 下一帧之前执行
high 在不久的将来立即执行
low 稍微延迟(100-200ms)执行也没关系
offscreen 下一次 render 时或 scroll 时才执行

生命周期

生命周期函数也被分为 2 个阶段了:

// 第 1 阶段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate

// 第 2 阶段 commit
componentDidMount
componentDidUpdate
componentWillUnmount

第 1 阶段的生命周期函数可能会被多次调用,默认以 low 优先级 执行,被高优先级任务打断的话,稍后重新执行。

Fiber 架构对 React 开发影响

本段主要探讨 React V16 后 Fiber 架构对我们使用 React 业务编程的影响有哪些?实际编码需要注意哪些内容。

1. 不使用官方宣布弃用的生命周期。

为了兼容旧代码,官方并没有立即在 V16 版本废弃三生命周期,用新的名字 (带上 UNSAFE) 还是能使用。建议使用了 V16+ 版本的 React 后就不要再使用废弃的三生命周期。
因为 React 17 版本将真正废弃这三生命周期:

到目前为止 (React 16.4),React 的渲染机制遵循同步渲染:
1) 首次渲染: willMount > render > didMount,
2) props 更新时: receiveProps > shouldUpdate > willUpdate > render > didUpdate
3) state 更新时: shouldUpdate > willUpdate > render > didUpdate
3) 卸载时: willUnmount
期间每个周期函数各司其职,输入输出都是可预测,一路下来很顺畅。
BUT 从 React 17 开始,渲染机制将会发生颠覆性改变,这个新方式就是 Async Render。
首先,async render 不是那种服务端渲染,比如发异步请求到后台返回 newState 甚至新的 html,这里的 async render 还是限制在 React 作为一个 View 框架的 View 层本身。
通过进一步观察可以发现,预废弃的三个生命周期函数都发生在虚拟 dom 的构建期间,也就是 render 之前。在将来的 React 17 中,在 dom 真正 render 之前,React 中的调度机制可能会不定期的去查看有没有更高优先级的任务,如果有,就打断当前的周期执行函数 (哪怕已经执行了一半),等高优先级任务完成,再回来重新执行之前被打断的周期函数。这种新机制对现存周期函数的影响就是它们的调用时机变的复杂而不可预测,这也就是为什么”UNSAFE”。
作者:辰辰沉沉大辰沉
来源:CSDN

2. 注意 Fiber 优先级导致的 bug;

了解 Fiber 原理后,业务开发注意高优先级任务频率,避免出现低优先级任务延迟太久执行或永不执行 bug(starvation:低优先级饿死)。

3. 业务逻辑实现别太依赖生命周期钩子函数;

在 Fiber 架构中,task 有可能被打断,需要重新执行,某些依赖生命周期实现的业务逻辑可能会受到影响。

参考文档
React 新生命周期;
深入理解进程和线程;
完全理解 React Fiber;
React Fiber;
如何阅读大型项目源码;
React 源码解析;
React Fiber 源码解析;
React Fiber 是什么?
React diff 算法策略和实现
React 新引擎,React Fiber 是什么?

正文完
 0

浅谈ReactFiber

15次阅读

共计 3223 个字符,预计需要花费 9 分钟才能阅读完成。

1. 什么是 fiber
每一个 ReactElement 都有一个对应的 fiber,记录这个节点的各种状态,fiber 是一链表的结构的串联起来。
2. Fiber 的组成
export type Fiber = {|

// Tag identifying the type of fiber.
// 区分 fiber 的种类
tag: WorkTag,

// Unique identifier of this child.
// 像 react 元素中的唯一的 key
key: null | string,

// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
// 就是 creatElement 的第一个值,用来在子节点 reconciliation 阶段的标识
elementType: any,

// The resolved function/class/ associated with this fiber.
// 异步组件加载 resovled 后种类是函数式还是类
type: any,

// The local state associated with this fiber.
// 与这个 fiber 联系的本地状态,指向实例
stateNode: any,

// It is conceptually the same as the return address of a stack frame.
// 指向 Fiber Tree 中的父节点
return: Fiber | null,

// Singly Linked List Tree Structure.
// 指向第一个子节点
child: Fiber | null,
// 指向兄弟节点
sibling: Fiber | null,
index: number,

// The ref last used to attach this node.
// I’ll avoid adding an owner field for prod and model that as functions.
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

// Input is the data coming into process this fiber. Arguments. Props.
// 新的即将进来的 props
pendingProps: any, // This type will be more specific once we overload the tag.
// 现在的已经展示在 UI 上的 props
memoizedProps: any, // The props used to create the output.

// A queue of state updates and callbacks.
// 保存更新的状态和回调函数
updateQueue: UpdateQueue<any> | null,

// The state used to create the output
// 展示在 UI 中的 state
memoizedState: any,

// A linked-list of contexts that this fiber depends on
contextDependencies: ContextDependencyList | null,

mode: TypeOfMode,

// Effect
// 副作用
effectTag: SideEffectTag,

// Singly linked list fast path to the next fiber with side-effects.
// 单链表结构,方便遍历 Fiber Tree 上有副作用的节点
nextEffect: Fiber | null,

// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
// 在子节点中的第一个和最后一个的副作用,这个可以允许我们进行切片的复用
firstEffect: Fiber | null,
lastEffect: Fiber | null,

// Represents a time in the future by which this work should be completed.
// Does not include work found in its subtree.
expirationTime: ExpirationTime,

// This is used to quickly determine if a subtree has no pending changes.
childExpirationTime: ExpirationTime,

// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,

// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don’t bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,

// If the Fiber is currently active in the “render” phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,

// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,

// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the “complete” phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,

// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,

// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
|};

正文完
 0