“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

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

评论

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

JavaScript刷LeetCode拿offer-js版字典

Geek_07a724

JavaScript LeetCode

【鲲鹏BoostKit】OminiRuntime ——高效统一的大数据分析Runtime底座

Geek_2d6073

简单两步,使用 cache 加快极狐GitLab CI/CD 构建速度

极狐GitLab

DevOps CI/CD cache runner 极狐GitLab

Element UI 省市区数据联动

源字节1号

微信小程序 软件开发 前端开发 后端开发

深入分析React-Scheduler原理

xiaofeng

React

几个你必须知道的React错误实践

xiaofeng

React

Vue虚拟dom是如何被创建的

yyds2026

Vue

javascript 高级编程 之 Array 用法总结

hellocoder2029

JavaScript

彻底搞懂React-hook链表构建原理

夏天的味道123

React

webpack热更新原理(面试大概率会问)

Geek_02d948

webpack

webpack模块化的原理

Geek_02d948

webpack

技术指南 | 如何集成Perforce版本控制系统Helix Core (P4V) 与软件生命周期管理工具Helix ALM

龙智—DevSecOps解决方案

版本控制 软件开发生命周期 版本管理 ALM

vue3实战-完全掌握ref、reactive

yyds2026

Vue

Nodejs+Redis实现简易消息队列

coder2028

node.js

javascript尾递归优化

hellocoder2029

JavaScript

webpack配置优化,让你的构建速度飞起

Geek_02d948

webpack

人工智能自然语言处理之Transformer阐述

XiaoChao_AI

人工智能 自然语言处理 Transformer 11月月更

vue中的几个高级概念

yyds2026

Vue

云栖盘点:2022 云网络产业干货分享

云布道师

云网络 云栖大会

同事每天早下班,原来是用了这8个开发工具

慕枫技术笔记

后端 开发 11月月更

人工智能机器学习之Bagging算法

XiaoChao_AI

人工智能 机器学习 11月月更

升级到React-Router-v6

xiaofeng

React

Nodejs:ESModule和commonjs,傻傻分不清

coder2028

node.js

Nodejs相关ORM框架分析

coder2028

node.js

js作用域、作用域链和它的一些优化

hellocoder2029

JavaScript

网易数帆加入星策社区,携手推进企业智能化转型进程

星策开源社区

开源 AI 开源社区 企业转型 数智化转型

人工智能机器学习之Boosting算法

XiaoChao_AI

人工智能 机器学习 11月月更

为啥不适合,依然有很多人大张旗鼓搞企业内部开源?(下)

laofo

研发效能

在线研讨会报名 | 如何通过自动化测试实现降本、增效与提质

龙智—DevSecOps解决方案

自动化测试 研讨会

​GOPS演讲 | 如何构建现代运营与支持体系,实现团队的高效协同

龙智—DevSecOps解决方案

gops ITSM ITSM解决方案 GOPS全球运维大会

个保法一周年:APP监听?算法为洪水猛兽?看看专家如何解读

科技热闻

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