React Fiber

更新日期: 2019-07-11阅读: 2.5k标签: 线程

背景

前段时间准备前端招聘事项,复习前端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 有可能被打断,需要重新执行,某些依赖生命周期实现的业务逻辑可能会受到影响。

原文:https://segmentfault.com/a/1190000020035950


链接: https://www.fly63.com/article/detial/4632

理解的线程、进程的关系与区别

进程就是一个应用程序在处理机上的一次执行过程,它是一个动态的概念,而线程是进程中的一部分,进程包含多个线程在运行。一简言之: 进程就是一个应用程序在处理机上的一次执行过程,它是一个动态的概念,而线程是进程中的一部分,进程包含多个线程在运行。

javascript中的伪线程,使用setTimeout模拟一个多线程

浏览器的内核是多线程的,一个浏览器一般至少实现三个常驻线程:javascript引擎线程,GUI渲染线程,浏览器事件触发线程。当我们要循环过百万级的数据甚至亿的时候怎么办?那就用setTimeout模拟一个多线程。

聊聊 JavaScript 与浏览器的那些事 - 引擎与线程

对 JavaScript 解释器和浏览器的线程机制理解的不是特别透彻,很容易混淆浏览器多线程机制并错误认为由于 Web Worker 的设计使得 JavaScript 拥有了多线程的能力。事后搜了不少资料进行学习,整理成此文,主要介绍浏览器的各个引擎、线程间的工作机制以及 JavaScript 单线程的一些事。

浏览器进程线程

进程是正在运行的程序的实例;线程(英语:thread)是操作系统能够进行运算调度的最小单位。可以打开任务管理器,可以看到有一个后台进程列表。这里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及cpu占有率。

JavaScript多线程编程

浏览器端JavaScript是以单线程的方式执行的,也就是说JavaScript和UI渲染占用同一个主线程,那就意味着,如果JavaScript进行高负载的数据处理,UI渲染就很有可能被阻断,浏览器就会出现卡顿,降低了用户体验。

Node.js 多线程完全指南

很多人都想知道单线程的 Node.js 怎么能与多线程后端竞争。考虑到其所谓的单线程特性,许多大公司选择 Node 作为其后端似乎违反直觉。要想知道原因,必须理解其单线程的真正含义。

JavaScript Event Loop和微任务、宏任务

JavaScript的一大特点就是单线程, 同一时间只能做一件事情,主要和它的用途有关, JavaScript主要是控制和用户的交互以及操作DOM。注定它是单线程。 假如是多个线程, 一个移除DOM节点,一个新增DOM节点,浏览器以谁的为准呢?

如何理解JS的单线程?

JS本质是单线程的。也就是说,它并不能像JAVA语言那样,两个线程并发执行。 但我们平时看到的JS,分明是可以同时运作很多任务的,这又是怎么回事呢?

理解JS执行顺序

众所周知,JS的执行顺序是自上而下的。 严格意义上来说,javascript没有多线程的概念,所有的程序都是单线程依次执行的。 就是代码在执行过程中,另一段代码想要执行就必须等当前代码执行完成后才可以进行。

初始WebWorker

JS单线程:我们都知道JavaScript它是一个单线程的语言,同一时间只能做一件事。比如:在浏览器中,某一时刻我们在操作DOM,你们这个时刻我们就不能去运行JavaScript代码,反过来也是,当我们在运行JavaScript代码的时候

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!