QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

解析 React 性能利器 — Fiber

  • 2021-06-15
  • 本文字数:6381 字

    阅读完需:约 21 分钟

解析 React 性能利器 — Fiber

什么是刷新率?


大部分显示器屏幕都有固定的刷新率(比如最新的一般在 60Hz),所以浏览器更新最好是在 60fps。如果在两次硬件刷新之间浏览器进行两次重绘是没有意义的只会消耗性能。 浏览器会利用这个间隔 16ms(一帧)适当地对绘制进行节流,如果在 16ms 内做了太多事情,会阻塞渲染,造成页面卡顿, 因此 16ms 就成为页面渲染优化的一个关键时间。


一帧做了哪些事情



  • events: 点击事件、键盘事件、滚动事件等

  • macro: 宏任务,如 setTimeout

  • micro: 微任务,如 Promise

  • rAFrequestAnimationFrame


window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。


  • Layout: CSS 计算,页面布局

  • Paint: 页面绘制

  • rIC: requestIdleCallback


window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout,则有可能为了在超时前执行函数而打乱执行顺序


一个帧内要做这么多事情…… 如果 js 执行时间过长超过 16ms,就会 block 住,那么就会丢掉一次帧的绘制

宏任务的执行总在微任务之后,但是与其他的顺序不太确定

TIPS:协调的概念:比较虚拟 DOM 树,找出需要变更的节点,更新,称为协调(Reconcliation)


React16 之前的协调



特点:

  • 递归调用,通过 React DOM 树级关系构成的栈递归

  • 在 virtualDOM 的比对过程中,发现一个 instance 有更新,会立即执行 DOM 操作。

  • 同步更新,没发打断

  • 代码示例


有下面这样一个 Component 组件,用他来模拟 DOM Diff 过程:

const Component = (  <div id="A1">    <div id="B1">      <div id="C1"></div>      <div id="C2"></div>    </div>    <div id="B2"></div>  </div>)
复制代码

Diff 过程:

上面定义的 Component 组件会首先通过 Babel 转成 React.CreateElement 生成 ReactElement,也就是我们口中的虚拟 DOM(virtualDOM),如下类似 root 的结构(下面里面属性做了很多简化,只展示了结构)。

let root = {  key: 'A1',  children: [    {      key: 'B1',      children: [        {          key: 'C1',          children: [],        },        {          key: 'C2',          children: [],        }      ],    },    {      key: 'B2',      children: [],    }  ],};
// 深度优先遍历function walk(vdom) {  doWork(vdom);  vdom.children.forEach(child => {    walk(child);  })}// 更新操作function doWork(vdom) {  console.log(vdom.key);}
walk(root);
复制代码

缺点:

根据上面代码会发现,如果有大量更新或者有很深的组件结构树,执行 diff 操作的执行栈会越来越深并不能及时释放,那么 js 将一直占用主线程,一直要等到整棵 virtualDOM 树计算完成之后,才能把执行权交给渲染引擎,这就会导致用户的交互操作以及页面动画得不到响应,就会有明显感觉卡顿(掉帧),影响用户体验。

  • 解决:

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会,所以 React 在 15 版本更新 16 版本时候推出了 Fiber 协调的概念。


Fiber 概念


Fiber 是对 React 核心算法的重构,2 年重构的产物就是 Fiber Reconciler


核心目标:扩大其适用性,包括动画,布局和手势。



  • 把可中断的工作拆分成小任务

  • 对正在做的工作调整优先次序、重做、复用上次(做了一半的)成果

  • 在父子任务之间从容切换(yield back and forth),以支持 React 执行过程中的布局刷新

  • 支持 render() 返回多个元素

  • 更好地支持 error boundary


每一个 Virtual DOM 节点内部都会生成对应的 Fiber。


Fiber 前置知识


怎么中断一个任务:实现一个类似于 Fiber 可中断的 workLoop。


function sleep(delay) {  for (let start = Date.now(); Date.now() - start <= delay;) {}}
// 每一个子项可以认为是一个 fiberconst works = [  () => {    console.log('第一个任务开始');    sleep(20);    console.log('第一个任务结束');  },  () => {    console.log('第 2 个任务开始');    sleep(20);    console.log('第 2 个任务结束');  },  () => {    console.log('第 3 个任务开始');    sleep(20);    console.log('第 3 个任务结束');  },];
window.requestIdleCallback(workLoop, { timeout: 1000});
function workLoop(deadLine) {  console.log('本帧的剩余时间剩', parseInt(deadLine.timeRemaining()));  /*  * deadLine {  *   timeRemaining(), 返回此帧还剩下多少 ms 供用户使用  *   didTimeout 返回 cb 任务是否超时  * }  */  while ((deadLine.timeRemaining() > 0 || deadLine.didTimeout) && works.length > 0) { // 对象 两个属性 timeRemaining()    performUnitOfWord();  }
  if (works.length > 0) {    window.requestIdleCallback(workLoop, { timeout: 1000});  }}
function performUnitOfWord() {  works.shift()(); // 取出第一个元素执行}
复制代码


  • 单链表

  • 存储数据的数据结构

  • 数据以节点的形式表示,每个节点的构成:元素 + 指针(后续元素存储位置),元素就是存储数据的存储单元

  • 单链表是 Fiber 中很重要的一个数据结构,很多异步更新逻辑都是通过单链表结构来实现的(setState 中的 UpdateQueue 更新链表也是基于单链表结构)


模拟一个类似 React 中 setState 批量更新的逻辑。


/**  Fiber 很多地方用到链表(单链表),尾指针没有指向 */
class Update {  constructor(payload, nextUpdate) {    this.payload = payload;    this.nextUpdate = nextUpdate; // 下一个节点的指针  }}
class UpdateQueue {  constructor(payload) {    this.baseState = null; // 原状态    this.firstUpdate = null; // 第一次更新    this.lastUpdate = null; // 最后一次更新  }
  enqueueUpdate(update) {    if (this.firstUpdate === null) {      this.firstUpdate = this.lastUpdate =update;    } else {      this.lastUpdate.nextUpdate = update;      this.lastUpdate = update;    }  }  // 获取老状态,遍历链表,进行更新  forceUpdate() {    let currentState = this.baseState || {}; // 初始状态    let currentUpdate = this.firstUpdate;    while (currentUpdate) {      let nextState = typeof currentUpdate.payload === 'function'                      ? currentUpdate.payload(currentState)                      : currentUpdate.payload;      currentState = {        ...currentState,        ...nextState,      }; // 使用当前更新得到最新的状态      currentUpdate = currentUpdate.nextUpdate; // 找下一个节点    }    this.firstUpdate = this.lastUpdate = null; // 更新结束清空链表    this.baseState = currentState;    return currentState;  }}// 链表可以中断和恢复// 每次 setState 都会通过一个链表保存起来,最后合并// enqueueUpdate 可以类比为 setState 操作let queue = new UpdateQueue();queue.enqueueUpdate(new Update({ name: '微医集团' }));queue.enqueueUpdate(new Update({ number: 0 }));queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));console.log(queue)queue.forceUpdate();
复制代码



思考:为什么 setState 在合成事件中会是异步去更新的? 解释:我们通过伪代码发现,每次的 setState 并没有对 UpdataQueue 中的 state 做任何更新,只是把每次需要更新的值(或函数),放到了 UpdataQueue 的链表上面,在执行 forceUpdate 的时候再做统一处理,处理完之后更新 state,所以没有执行 forceUpdate 之前,我们拿到的 state 都不是我们预期想要的 state。


React 中的 Fiber


  1. Fiber 的两个执行阶段

  2. 协调 Reconcile(render):对 virtualDOM 操作阶段,对应到新的调度算法中,就是通过 Diff Fiber Tree 找出要做的更新工作生成 Fiber 树。这是一个 js 计算过程,计算结果可以被缓存,计算过程可以被打断,也可以恢复执行。 所以,React 介绍 Fiber Reconciler 调度算法时,有提到新算法具有可拆分、可中断任务的新特性,就是因为这部分的工作是一个纯 js 计算过程,所以是可以被缓存、被打断和恢复的

  3. 提交更新 commit: 渲染阶段,拿到更新工作,提交更新并调用对应渲染模块(React-DOM)进行渲染。为了防止页面抖动,该 过程是同步且不能被打断

  4. React 中定义一个组件用来创建 Fiber。

const Component = (  <div id="A1">    A1    <div id="B1">      B1      <div id="C1">C1</div>      <div id="C2">C2</div>    </div>    <div id="B2">B2</div>  </div>)
复制代码


  1. 上面定义的 Component 是一个组件,babel 解析时候会默认调用 React.createElement()方法,最终生成下面代码所示这样的 virtualDOM 结构并传给 ReactDOM.render()方法进行调度。

{  "type":"div",  "key":null,  "ref":null,  "props": {    "id":"A1",    "children":[      "A1",      {        "type":"div",        "key":null,        "ref":null,        "props":{          "id":"B1",          "children":[            "B1",            {              "type":"div",              "key":null,              "ref":null,              "props":{                  "id":"C1",                  "children":"C1"              },              "_owner":null,              "_store":{
} }, { "type":"div", "key":null, "ref":null, "props":{ "id":"C2", "children":"C2" }, "_owner":null, "_store":{
} } ] }, "_owner":null, "_store":{
} }, { "type":"div", "key":null, "ref":null, "props":{ "id":"B2", "children":"B2" }, "_owner":null, "_store":{
} } ] }, "_owner":null, "_store":{
}}
复制代码
  1. render 方法会接受 Virtual DOM,为每个 Virtual DOM 创建 Fiber(render 阶段),并且按照一定关系连接接起来。

  2. fiber 结构

class FiberNode {  constructor(tag, pendingProps, key, mode) {    // 实例属性    this.tag = tag; // 标记不同组件类型,如 classComponent,functionComponent    this.key = key; // react 元素上的 key 就是 jsx 上写的那个 key,也就是最终 ReactElement 上的    this.elementType = null; // createElement 的第一个参数,ReactElement 上的 type    this.type = null; // 表示 fiber 的真实类型 ,elementType 基本一样    this.stateNode = null; // 实例对象,比如 class 组件 new 完后就挂载在这个属性上面,如果是 RootFiber,那么它上面挂的是 FiberRoot
// fiber this.return = null; // 父节点,指向上一个 fiber this.child = null; // 子节点,指向自身下面的第一个 fiber this.sibling = null; // 兄弟组件, 指向一个兄弟节点 this.index = 0; // 一般如果没有兄弟节点的话是 0 当某个父节点下的子节点是数组类型的时候会给每个子节点一个 index,index 和 key 要一起做 diff
this.ref = null; // reactElement 上的 ref 属性
this.pendingProps = pendingProps; // 新的 props this.memoizedProps = null; // 旧的 props this.updateQueue = null; // fiber 上的更新队列 执行一次 setState 就会往这个属性上挂一个新的更新, 每条更新最终会形成一个链表结构,最后做批量更新 this.memoizedState = null; // 对应 memoizedProps,上次渲染的 state,相当于当前的 state,理解成 prev 和 next 的关系
this.mode = mode; // 表示当前组件下的子组件的渲染方式
// effects
this.effectTag = NoEffect; // 表示当前 fiber 要进行何种更新 this.nextEffect = null; // 指向下个需要更新的 fiber this.firstEffect = null; // 指向所有子节点里,需要更新的 fiber 里的第一个 this.lastEffect = null; // 指向所有子节点中需要更新的 fiber 的最后一个
this.expirationTime = NoWork; // 过期时间,代表任务在未来的哪个时间点应该被完成 this.childExpirationTime = NoWork; // child 过期时间
this.alternate = null; // current 树和 workInprogress 树之间的相互引用 }}
复制代码


Fiber 有很多属性,所有子节点 Fiber 的连接接是通过 child,return,siblint 链接起来,alternate 连接的是每一次更新的状态,用来对比每次状态更新以及缓存,我们使用节点的 id 来标识每个 Fiber 组件,转换为 Fiber 最终会生成如下图所示的结构,也是类似于 virtualDOM 结构的,构建的顺序是先 child => sibling => return,如果当前节点没有 child 了,这个节点就会完成。



Fiber 树


  • 收集依赖


收集依赖是在生成 Fiber 过程 (render 阶段) 中同时完成的,按照每个节点完成的顺序来构建链表,每个有了 Fiber 的组件通过自己的 nextEffect 指向下一个需要更新的组件,每一个父节点都有 firstEffect 和 lastEffect 来连接自己子节点的第一次更新和最后一次更新,最终会生成下图这样的更新链表。


副作用链表(更新链表)


  • 提交更新 commit

全部节点创建完 Fiber 之后,会进入 commit 阶段,会从 root 的 fistEffect(所有节点的第一个副作用阶段)开始更新,然后找 firstEffect 的 nextEffect 节点,以此类推,一气呵成全部更新完,然后清空更新链表,完成此次更新,这个过程不可打断。


总结


以上是 React 大概工作流程,主要以首次更新全部节点需要创建 Fiber 来讨论,后续会更新:基于 Fiber 的 diff、React 中合成事件、各种类型组件 (类组件,Function 组件)、hooks、事件优先级(expirationTime) 在内部如何调度相关。



头图:Unsplash

作者:武晓慧

原文:https://mp.weixin.qq.com/s/63YJi2Y3tX8CFsXuJqT9dQ

原文:解析 React 性能利器 — Fiber

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-06-15 08:002896

评论

发布
暂无评论
发现更多内容

Parallels Desktop 19 for Mac 一键激活版(PD19虚拟机)

iMac小白

如何用 Python 实现一个 “系统声音” 的实时律动挂件

北桥苏

Python pyaudio 系统声音

左耳听风-我的三观「读书打卡 day 01」

Java 工程师蔡姬

程序员 读书 读书感悟 左耳朵耗子

如何基于文心一言NLP大模型搭建生成式智能对话服务

百度开发者中心

人工智能 nlp 大模型

Parallels Desktop 18 for Mac v18.3.2永久激活版下载

iMac小白

Axure RP 8 中文激活授权码 Axure RP 8 完整安装教程 Mac/win

南屿

原型设计 Axure RP 8授权码 Axure RP 8下载 Axure RP 8汉化包

苹果Mac自用软件推荐:Unite for mac 将网站转化为应用程序 支持M1/M2/Intel

南屿

Unite for Mac Unite破解版 将网站转化为应用程序 Mac软件资源站

行云部署成长之路--慢SQL优化之旅 | 京东云技术团队

京东科技开发者

【最新中文版激活序列号】Macs Fan Control Pro 苹果电脑掌控风扇必备软件

南屿

Macs Fan Control Pro下载 Macs Fan Control Pro破解 Mac 电脑风扇速度

CloudMounter for mac v4.3 激活版下载(云盘本地加载工具)

iMac小白

每日一题:LeetCode-10037. 移除后集合的最多元素数

Geek_4z9ami

Go 面试 算法 LeetCode 贪心算法

轻松完成图片转换矢量图,推荐Vector Magic for Mac破解版

南屿

mac软件下载 Vector Magic破解版 矢量图片转换工具

有关SCADA系统的所有信息:什么是SCADA?

2D3D前端可视化开发

物联网 组态软件 工业自动化 SCADA HMI

快麦ERP退货借助APPlink快速同步CRM

RestCloud

零代码 CRM ERP APPlink

3D建模设计 Vectorworks 2022 SP5激活版 for Mac 下载安装教程

南屿

3D建模软件 Vectorworks 2022下载 破解软件 Vectorworks 2022注册码

Rhinoceros 6 for Mac(犀牛6) 6.31.20315完美激活版

mac

苹果mac Windows软件 Rhinoceros 3D设计软件 犀牛

Magnet for mac v2.14.0中文免激活版下载

iMac小白

实践案例:通过API优化加快上市时间

幂简集成

产品 MVP API OpenAPI REST API

软件测试开发/全日制/测试管理丨软件测试流程

测试人

软件测试 测试开发

DNA序列分析软件 SnapGene 5 v5.3.1中文激活版 下载安装

南屿

SnapGene 5破解版 SnapGene 5下载 分子生物学软件 DNA序列

One Switch mac版 菜单栏一键控制神器 支持M/Intel

南屿

mac效率工具 One Switch for Mac 菜单栏一键开关控制神器 One Switch破解

Premiere Pro 2022 for Mac v22.6.2中文激活版下载

iMac小白

StarRocks Awards 2023 年度贡献人物

StarRocks

数据库 数据分析 开源社区 StarRocks

Emby for Mac:多媒体影音库管理工具 兼容M1

南屿

苹果软件下载 Mac破解软件 Emby for Mac 多媒体影音库管理工具

Fig Player - play mp4 mkv mp3 苹果电脑媒体播放器

南屿

mac视频播放器 苹果软件下载 Fig Player破解下载 Fig Player Mac版

苹果电脑YouTube客户端下载:YouTube for mac激活版v1.22(56)中文版安装教程

南屿

YouTube客户端 苹果mac软件 Clicker for YouTube

深入理解TF-IDF、BM25算法与BM25变种:揭秘信息检索的核心原理与应用

汀丶人工智能

nlp 搜索系统 BM25算法 关键词检索

TikTok直播专线的优势及应用价值

Ogcloud

直播 直播优化 TikTok

解析 React 性能利器 — Fiber_语言 & 开发_微医大前端技术_InfoQ精选文章