浅谈React Fiber

react_fiber.jpeg

背景

前段时间准备前端招聘事项,复习前端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 架构后是 F****iber 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.png

网友测试使用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.png

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源码分析 第三篇(异步状态)

优先级

image.png
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时才执行

生命周期

image.png

生命周期函数也被分为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 新生命周期;
深入理解进程和线程;
完全理解React Fiber;
React Fiber;
如何阅读大型项目源码;
React 源码解析;
React Fiber 源码解析;
React Fiber 是什么?
React diff 算法策略和实现
React 新引擎, React Fiber 是什么?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容