写点什么

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

评论

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

Snagit for mac(最强大的屏幕截图软件) v2024.2.1中文版

iMac小白

智能商品管理系统:驱动零售盈利型的数据利器

第七在线

Excel 2021 LTSC for Mac(excel表格) v16.83中文正式版

iMac小白

RocketMQ为什么这么快?我从源码中扒出了10大原因!

不在线第一只蜗牛

源码 RocketMQ

游戏发行新方式——游戏海外发行代理

Ogcloud

游戏 云游戏 游戏发行 游戏发行公司

程序员来看,你选哪一门编程语言提升自己?

高端章鱼哥

TikTok直播畅通无阻,海外直播专线打造稳定流畅的网络环境

Ogcloud

TikTok 海外直播专线 海外直播 tiktok直播 tiktok直播专线

光伏储能制造业生产管理mes系统解决方案

万界星空科技

制造业 mes 能源行业 万界星空科技 光伏

亚马逊扣店租/注册店铺可以使用虚拟卡吗?

跨境

亚马逊

TG Pro for mac(Mac硬件温度检测工具) v2.92激活版

iMac小白

利用Nginx正向代理实现局域网电脑访问外网

EquatorCoco

php nginx 服务器 局域网

用Python编写自己的微型Redis

快乐非自愿限量之名

Python redis

Perfectly Clear Workbench for Mac(智能图像清晰修复软件) v4.6.0.2642永久激活版

iMac小白

Allavsoft for Mac(优秀的视频下载工具)v3.26注册激活版

iMac小白

如何仅用3行代码,搞定业务敏感数据加解密?

阿里云瑶池数据库

数据库 云计算 阿里云 数据安全 polarDB

Prompt进阶3:LangGPT(构建高性能质量Prompt策略和技巧2)--稳定高质量文案生成器

汀丶人工智能

prompt 工程

15 年云数据库老兵DuckDB CEO:数据库圈应告别“唯性能论”

酷克数据HashData

Prompt进阶2:LangGPT(构建高性能Prompt策略和技巧)--最佳实践指南

汀丶人工智能

prompt 工程 Prompt Engineering

读取速率十倍于 Elasticsearch,TDengine 在知轮科技智慧轮胎系统中的应用

TDengine

开源LLM大模型聊天

百度开发者中心

人工智能 大模型 问答助手

Microsoft Outlook 2021 LTSC for Mac v16.83中文正式版

iMac小白

FotoMagico for Mac(专业幻灯片制作软件) v6.5.8激活版

iMac小白

Word 2021 LTSC for Mac(word 2021) v16.83正式激活版

iMac小白

Navicat for SQLite 16 for Mac(强大数据库管理及开发工具) v16.3.7中文版

iMac小白

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