写点什么

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

评论

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

Easysearch压缩模式深度比较:ZSTD+source_reuse的优势分析

极限实验室

easysearch

充换电企业开迈斯低成本提升线上应用稳定性的最佳实践

阿里巴巴云原生

阿里云 云原生

可观测 AIOps 的智能监控和诊断实践丨QCon 全球软件开发大会总结

阿里巴巴云原生

阿里云 云原生 AIOPS 可观测

Python开发:pycharm pro 2023 永久激活秘钥【Mac/win】

Rose

Python开发 PyCharm破解版 PyCharm Pro密钥 JetBrainsPyCharm

苹果Mac文件管理浏览软件Path Finder中文破解版 支持Mac14系统

Rose

mac文件管理软件 Path Finder Path Finder破解

活在无限中

少油少糖八分饱

读后感 阅读 动漫 葬送的芙莉莲 有限与无限游戏

ARTS 打卡第6周

AI帅辉

ARTS 打卡计划 学习分享

Apache Dubbo 云原生可观测性的探索与实践

阿里巴巴云原生

Apache 阿里云 云原生 dubbo

Avid Sibelius Ultimate 2023 for Mac(西贝柳斯音乐记谱软件)

Rose

西贝柳斯终极解锁版 Avid Sibelius 2023 Mac Mac乐谱制作软件

文心一言 VS 讯飞星火 VS chatgpt (113)-- 算法导论10.2 5题

福大大架构师每日一题

福大大架构师每日一题

茶百道全链路可观测实战

阿里巴巴云原生

阿里云 云原生 可观测

启动速度提升 10 倍:Apache Dubbo 静态化方案深入解析

阿里巴巴云原生

阿里云 云原生

安装Linux系统对硬件的要求

芯动大师

基于 Triple 实现 Web 移动端后端全面打通

阿里巴巴云原生

阿里云 微服务 云原生

Defi/DAPP/LP代币流动性质押挖矿系统开发(技术组件)

V\TG【ch3nguang】

Illustrator 2024 for mac(标准矢量插画设计软件) v28.0完整激活版

mac

苹果mac Windows软件 矢量图形编辑软件 Illustrator 2024

DeFi/DAPP质押借贷分红挖矿系统开发/详情方案

V\TG【ch3nguang】

如何转产品工程师?

IC男奋斗史

职业规划 芯片 半导体 芯片测试 ATE测试

蓝易云:Centos 7 通过 targz文件安装 Elastic Search服务教程!

百度搜索:蓝易云

elasticsearch Linux centos SEO targz

Premiere Pro 2024 for mac(pr2024视频编辑器) v24.0完整激活版

mac

苹果mac Windows软件 视频剪辑软件 Premiere Pro 2024

CSS小技巧之单标签loader

南城FE

CSS css3 前端 Loader

几款好用的苹果Mac硬盘检测工具

Rose

SSD mac软件下载 Mac硬盘健康 硬盘检测软件

Downie 4 for Mac(最好用的视频下载软件) 4.6.31中文激活版

mac

Downie4 苹果mac Windows软件 网站视频下载

Lightroom Classic 2024更新,最新LRC2024中文激活版下载mac/win

iMac小白

Lightroom Classic2024 LrC2024

【论文阅读】【三维场景点云分割】Superpoint Transformer for 3D Scene Instance Segmentation

AI帅辉

深度学习 论文阅读 Transformer 分割 3D点云

不抖机灵!让工程师来告诉你做芯片是如何烧钱的!

IC男奋斗史

芯片 半导体 芯片测试 ATE测试

OpenResty 入门以及 WAF 防御实战

越长大越悲伤

nginx openresty waf

蓝易云:Redis相比Memcached有哪些优势?

百度搜索:蓝易云

redis memcached 云计算 Linux 云服务器

ATE机台哪家强?

IC男奋斗史

芯片 半导体 职场经验 芯片测试 ATE测试

Bridge 2024 (BR)新功能介绍及破解安装教程

Rose

Adobe Bridge 2024 BR2024下载 Bridge 2024破解版 Bridge 2024 中文版

Python - 字典2

小万哥

Python 程序员 软件 后端 开发

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