写点什么

解析 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:002950

评论

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

流媒体:依托于声网的连麦解决方案

小岛上的黑桃六

架构 音视频 架构师 流媒体 声网

GitHub开源的最全中文诗歌古典文集数据库

不脱发的程序猿

GitHub 开源 程序人生 中华古典文集数据库

万丈高楼平地起,爆肝21000字Java基础知识总结,收藏起来总有用得着的时候

北游学Java

Java 集合 线程池 IO流

yarn的多租户配置实现资源隔离

五分钟学大数据

YARN

负载均衡续:万亿流量场景下的负载均衡实践

Coder的技术之路

负载均衡 架构 高并发 负载均衡架构

高并发系列:架构优化之从BAT实际案例看消息中间件的妙用

Coder的技术之路

高并发 高并发系统设计 消息队列 消息中间件

从简历被拒到收割8个大厂offer,我用了3个月成功破茧成蝶

比伯

Java 编程 架构 面试 计算机

架构师实战营,模块三:架构设计详细文档

ifc177

#架构实战营

zookeeper的架构

大数据技术指南

zookeeper 5月日更

架构实战营模块三作业

日照时间长

架构实战营

数据仓库分层架构及元数据管理

五分钟学大数据

数据仓库

云图说|云数据库MySQL内核小版本升级全攻略

华为云开发者联盟

MySQL 云数据库 内核 华为云数据库 小版本升级

HDFS

xujiangniao

大四实习生”都四面成功拿到字节跳动Offer了,你还有什么理由去摸鱼?

学Java关注我

Java 编程 架构 面试 计算机

Java面试:BIO,NIO,AIO 的区别,别再傻傻分不清楚

Java大蜗牛

Java 程序员 面试 编程语言 后端

Flink on Zeppelin 系列之:Yarn Application 模式支持

Apache Flink

flink

百分点大数据技术团队:政务数据安全管理实践

百分点大数据团队

YARN资源调度三种模型介绍

五分钟学大数据

YARN

LiteOS内核源码分析:静态内存Static Memory

华为云开发者联盟

内存管理 LiteOS 静态内存 Static Memory Membox

INNOVATE 2021 圆满落幕,一起盘点那些 AI 前沿实例!

亚马逊云科技 (Amazon Web Services)

回顾 Alex Smola 讲述的“自动化机器学习(AutoML)”,本文带你了解 AutoGluon!

亚马逊云科技 (Amazon Web Services)

高并发之存储篇:关注下索引原理和优化吧!躲得过实践,躲不过面试官!

Coder的技术之路

数据库 高并发 索引结构 索引优化

用机器学习操控无人驾驶小车,竟然和训练狗狗的原理一样!?

亚马逊云科技 (Amazon Web Services)

高并发系列:存储优化之也许可能是史上最详尽的分库分表文章之一

Coder的技术之路

高并发 分库分表 高并发系统设计

【LeetCode】解码异或后的数组Java题解

Albert

算法 LeetCode 5月日更

Android中绘制圆角的三种方式

teoking

android

GitHub Actions:真正的 DevOps CI

大龄程序员老羊

架构 DevOps 持续集成 Github Actions NoOps

【案例】新基建下星环科技城轨智能视频分析系统建设

星环科技

专家解惑 | 关于华为云盘古大模型,你想问的都在这里~

华为云开发者联盟

计算机视觉 nlp 华为云 盘古大模型 预训练

HDFS的HA以及Yarn的HA高可用

五分钟学大数据

hdfs YARN 5月日更

架构训练营-作业三(消息队列详细架构设计文档)

eoeoeo

架构实战营

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