初窥 React Fiber

简介: 初窥 React Fiber

一、为什么会有 Fiber


之前 React 的渲染任务是一个同步任务。同步的任务问题是,当执行该同步任务时,其他任务就需要等待,如果这个任务执行时间过长,就会造成任务阻塞,导致其他任务无法及时执行,比如页面滚动,就会导致页面出现卡顿的现象。


二、使用 Fiber 给 React 带来的好处是什么


  1. 可以自己控制渲染过程:
  2. 将渲染任务拆分为任务单元,实现增量渲染。
  3. 任务单元可以暂停、复用和终止。
  4. 可以按任务类型为任务单元设置优先级。
  5. 任务单元可以并发执行。
  6. 优化性能:
  7. 解决掉持续占用主线程的问题之后,优化了页面的体验性能,对一些体验要求较高的场景,比如动画、手势,更适用了。
  8. 顺便解决历史遗留问题:
  9. 方便处理组件异常情况,推出 componentDidCatch 生命周期函数。
  10. 支持 render() 函数返回数组元素,推出 Fragment 组件。

并且这些优化,对于用户来说基本是无感知的,不会 block 用户的使用。


三、Fiber 的实现思路


渲染一个 React 应用,其实是在调用一个函数,函数本身会调用其他函数,形成调用栈。这种递归调用产生的调用栈我们无法控制。而 Fiber 要做的事情是让函数调用栈可以按需要来运行,可以手动控制。所以 Fiber 可以理解为 reimplementation of the stack。


四、React 做了哪些改动


1. 深度优先遍历和节点之间是链表关系

首先 Fiber 使用深度优先遍历,来代替之前的递归调用,然后控制深度优先遍历的过程。

  1. 从根元素开始,不断遍历,先遍历子元素,看子元素是否有子元素,如果有则继续遍历,如果没有则开始渲染这个元素。
  2. 这个元素渲染完之后,检查是否有兄弟元素,如果有则遍历兄弟元素,重复步骤一。
  3. 如果没有兄弟元素则返回父元素,处理父元素。
  4. 然后一直往上回到根元素,处理根元素。

并且节点之间是一个链表关系,用来模拟函数调用栈。节点记录了它的父节点、子节点和它的最近的兄弟节点。我们可以拿它和函数调用栈对比一下:

函数调用栈 Fiber
基本单位 函数 fiber node
输入 函数参数 props
本地状态 本地变量 state
输出 函数返回值 react element
下级调用 嵌套函数调用 child node
上级调用 返回地址 return node

如上面的图表所示,fiber node tree 和函数调用栈一样,保存了节点处理的上下文信息,这样我们就可以手动控制节点的渲染过程了。


2. 为 React Element 创建 Fiber Node

在 reconciliation 期间,会为 render 方法返回的每个 react element 创建一个 fiber node,对应形成了一棵 fiber node 树,在随后的更新中,React 会重用 fiber 节点,并使用来自 react element 的数据来更新自身的属性,如果从 render 方法返回的 react element 有变化,react 会根据 key 来移动或者删除它。 这其中相当于改变树节点的数据结构,增加了很多属性:

  1. 描述层级关系:
  2. return,指向父节点。
  3. child,指向第一个子节点。
  4. sibling,指向下一个兄弟节点。
  5. stateNode 保存组件的实例。
  6. 副作用种类和副作用链表相关:
  7. effectTag,当需要变化的时候,具体需要执行的操作的类型,比如 Update,Placement。
  8. nextEffect,下一个需要处理的有副作用的 Fiber。
  9. firstEffect 和 lastEffect,本 Fiber 的子树中有副作用的第一个和最后一个 Fiber。
  10. 更新相关的:
  11. updateQueue,更新队列,用于记录状态更新,回调函数,DOM 更新的队列。
  12. memoizedState,上一次更新 fiber 的 state。
  13. memoizedProps,上一次更新 fiber 的 props。
  14. pendingProps,新的 props,将用于子组件或 DOM 元素的 props。
  15. 剩余执行时间相关的:
  16. expirationTime,一个任务单元的可执行时间。
  17. childExpirationTime,用来判断子树是否还有待完成的修改。


3. Effects 链表

React 能够非常快速地更新,并且为了实现高性能,它构建了一个有副作用的 fiber 节点的列表,能快速遍历出需要执行修改的节点,比遍历整颗树要快很多,并且不用在没有副作用的节点上花费时间。如下图:


image.png


你可以看到带有 effects 的节点是如何链接在一起的,当遍历节点时,React 使用 firstEffect 指针来确定 effects 链表的开始位置。所以上图可以表示为这样的线性链表:


image.png


4. 双缓冲


React 中的双缓冲是指,从旧 fiber 树到新 fiber 树,中间会构建 workInProgress 树,一旦 workInProgress 树构建完成提交,得到的就是新 fiber 树。这样做的好处是可以减少内存分配和垃圾回收,workInProgress 树的节点不全是新的,比如某颗子树不需要变动,React 会复用这颗子树,减少额外的操作。


双缓冲技术还有一个好处就是容易处理渲染过程中抛出的异常,当一个节点渲染发生错误时,可以沿用旧 fiber 树的节点,避免整个渲染过程崩掉。


5. 渲染过程分成了两个阶段

React fiber 的渲染过程分成 render 阶段和 commit 阶段,相当于之前的 diff 阶段和 patch 阶段。


render 阶段要完成的工作是不断循环遍历构建 workInProgress 树(构造中的 fiber 节点树),在 fiber 节点上标记 commit 阶段需要完成的工作。commit 阶段则是根据 workInProgress 树上的节点标记,处理 effects 链表,更新真实 DOM 树。一旦这个 workInProgress 树在屏幕上呈现,它就变成了新的 fiber 节点树。


对于生命周期函数来说,render 阶段会经历的生命周期函数:

[UNSAFE_]componentWillMount (即将废弃)、[UNSAFE_]componentWillReceiveProps (即将废弃)、getDerivedStateFromProps、shouldComponentUpdate、[UNSAFE_]componentWillUpdate (即将废弃)、render。commit 阶段会经历的生命周期函数是:getSnapshotBeforeUpdate、componentDidMount、componentDidUpdate、componentWillUnmount。


前面说到 React 期望手动控制渲染过程,可以暂停、终止和复用渲染过程。对于 render 阶段,可以这么手动控制渲染过程,所以在 render 阶段的生命周期函数有可能会运行多次,所以写代码的时候要注意,避免做有副作用的操作,产生 bug,也因为这样 React 计划废弃其中的一些生命周期函数。对于 commit 阶段则不同,它是一口气把工作做完(同步任务),中间不会暂停,所以尽量不要在这些生命周期里面进行复杂运算。

为什么 render 阶段可以手动控制渲染过程,而 commit 阶段则需要一次性全部把工作做完。是因为 React 的核心原则之一:一致性,它总是一次性更新 DOM,不会显示部分结果。render 阶段对用户不可见,所以可以一部分一部分的处理渲染任务,而 commit 阶段是把修改更新到 DOM 上,只能一次性更新完,所以需要一口气把所有工作都做完。


6. 任务单元优先级

上面说到可以按任务类型分配优先级,那么这个优先级怎么分呢?


五、Fiber 的调度


1. 自实现了一个 requestIdleCallback

requestIdleCallback() 方法,本身的功能是用来定义当浏览器主线程空闲时要处理的回调函数。fiber 确保不阻塞主线程的关键是在,每次分配一段空闲时间来渲染,时间到了之后,看现在的浏览器主线程是不是空闲的,如果是则继续分配一段空闲时间来渲染,如果不是则停止当前的渲染工作,让出浏览器主线程处理其他任务。所以会预想要用到 reqeustIdleCallback() 函数。 但是 requestIdleCallback 由于是新出的 API,很多浏览器还不支持,且有 issue 有谈到它实际上有点过于严格,并且执行的不够频繁,大概每秒只会执行 20 次,无法保证流畅的 UI 呈现,因此 React 团队决定借鉴它的思想实现了自己的版本,使用 requestAnimationFrame 方法来模拟。但是 requestAnimationFrame 方法有一个缺陷:页面处于后台时回调函数不会执行,因此需要有一个补救措施,使用 setTimeout 设定一个 100ms 的定时时间,去继续执行渲染任务。 接下主要讲 requestAnimationFrame 的实现思路。


2. Fiber 的调度过程

在未来,调度这个功能不会仅仅只有 React 才拥有。因为 React 已经尝试把调度这个模块单独抽离成一个库,这个库在未来能够被大家置入到自己的应用中去提高用户体验。 并且社区也有提案,希望浏览器能自带这方面的功能,具体可以阅读这个库


参考



目录
相关文章
|
6月前
|
前端开发 JavaScript 调度
【第32期】一文学会用React Fiber提升性能
【第32期】一文学会用React Fiber提升性能
108 0
|
21天前
|
前端开发 调度 UED
React 执行过程中 Fiber 的优先级是如何确定的?
【10月更文挑战第27天】React能够更加智能地管理任务的执行顺序,在保证用户交互体验的同时,充分利用系统资源,提高应用的整体性能和响应速度。
|
1月前
|
存储 前端开发 JavaScript
深入理解React Fiber架构及其性能优化
【10月更文挑战第5天】深入理解React Fiber架构及其性能优化
83 1
|
21天前
|
前端开发 JavaScript 调度
React 的 fiber
【10月更文挑战第26天】React Fiber 是 React 框架中的一个重要创新,它为 React 应用的性能优化和用户体验提升提供了强大的支持,使得 React 在处理复杂的前端应用场景时更加高效和灵活。
|
3月前
|
缓存 前端开发 JavaScript
理解 React 的 Fiber 架构
【8月更文挑战第6天】 理解 React 的 Fiber 架构
187 1
|
6月前
|
移动开发 前端开发 JavaScript
react fiber架构【详细讲解,看这一篇就够了】
react fiber架构【详细讲解,看这一篇就够了】
518 0
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
66 0
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
|
前端开发
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍2
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍2
59 0
|
前端开发
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍1
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍1
64 0
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍
69 0