解读 React Fiber

背景

前段时间准备前端招聘事项,复习前端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占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。

[图片上传失败...(image-b01a83-1576808461497)]

网友测试使用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 优化思路

[图片上传失败...(image-701697-1576808461497)]

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
  4. 卸载时: 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 Fiber)