六个问题让你更懂 React Fiber(上)

简介: 大家好,我是零一,很多人都摸不透React,看不懂源码,甚至不想看源码(确实很难看懂啊!),"霸王硬上弓" 肯定是不行呀,不如从React的整体架构或者说从最核心的Fiber开始了解,说不定能帮你更懂React呢!

正文


React Fiber 是Facebook花费两年余时间对 React 做出的一个重大改变与优化,是对 React 核心算法的一次重新实现。从Facebook在 React Conf 2017会议上确认,React Fiber 会在React 16 版本发布至今,也已过去三年有余,如今,React 17 业已发布,社区关于Fiber的优秀文章不在少数。


本文源于一次团队内部的技术分享,借鉴社区优秀文章,结合个人理解,进行整合,从六个问题出发,对 React Fiber 进行理解与认识,同时对时下热门的前端框架Svelte进行简要介绍与剖析,希望对正在探究 React 及各前端框架的小伙伴们能有所助益。


全文大量参考和引用以下几篇博文,读者可自行查阅:


  • React技术揭秘[1]


  • 前端工程师的自我修养:React Fiber 是如何实现更新过程可控的[2]


  • 新兴前端框架 Svelte 从入门到原理


  • 以 React 为例,说说框架和性能(下)[3


一、React 的设计理念是什么?


React官网在React哲学[4]一节开篇提到:


我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。React 最棒的部分之一是引导我们思考如何构建一个应用。


由此可见,React 追求的是 “快速响应”,那么,“快速响应“的制约因素都有什么呢?


  • CPU的瓶颈:当项目变得庞大、组件数量繁多、遇到大计算量的操作或者设备性能不足使得页面掉帧,导致卡顿。


  • IO的瓶颈:发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。


本文要聊的fiber 架构主要就是用来解决 CPU 和网络的问题,这两个问题一直也是最影响前端开发体验的地方,一个会造成卡顿,一个会造成白屏。为此 react 为前端引入了两个新概念:Time Slicing 时间分片和Suspense


二、React的“先天不足” —— 听说 Vue 3.0 采用了动静结合的 Dom diff,React 为何不跟进?


Vue 3.0 动静结合的 Dom diff


Vue3.0 提出动静结合的 DOM diff 思想,动静结合的 DOM diff其实是在预编译阶段进行了优化。之所以能够做到预编译优化,是因为 Vue core 可以静态分析 template,在解析模版时,整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签和文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。


借助预编译过程,Vue 可以做到的预编译优化就很强大了。比如在预编译时标记出模版中可能变化的组件节点,再次进行渲染前 diff 时就可以跳过“永远不会变化的节点”,而只需要对比“可能会变化的动态节点”。这也就是动静结合的 DOM diff 将 diff 成本与模版大小正相关优化到与动态节点正相关的理论依据


React 能否像 Vue 那样进行预编译优化?


Vue 需要做数据双向绑定,需要进行数据拦截或代理,那它就需要在预编译阶段静态分析模版,分析出视图依赖了哪些数据,进行响应式处理。而 React 就是局部重新渲染,React 拿到的或者说掌管的,所负责的就是一堆递归 React.createElement 的执行调用(参考下方经过Babel转换的代码),它无法从模版层面进行静态分析。JSX 和手写的 render function[5] 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足


JSX 写法:


<div>
  <h1>六个问题助你理解 React Fiber</h1>
  <ul>
    <li>React</li>
    <li>Vue</li>
  </ul>
</div>


递归 React.createElement:


// Babel转换后
React.createElement(
  "div",
  null,
 React.createElement(
    "h1",
    null,
    "\u516D\u4E2A\u95EE\u9898\u52A9\u4F60\u7406\u89E3 React Fiber"
 ),
 React.createElement(
    "ul",
    null,
   React.createElement("li", null, "React"),
   React.createElement("li", null, "Vue")
 )
);


JSX vs Template


57c4f1ce666ede7aa7b2c8d09a4ea8b6.png


  • JSX 具有 JavaScript 的完整表现力,可以构建非常复杂的组件。但是灵活的语法,也意味着引擎难以理解,无法预判开发者的用户意图,从而难以优化性能。


  • Template 模板是一种非常有约束的语言,你只能以某种方式去编写模板。


既然存在以上编译时先天不足,在运行时优化方面,React一直在努力。比如,React15实现了batchedUpdates(批量更新)。即同一事件回调函数上下文中的多次setState只会触发一次更新。



但是,如果单次更新就很耗时,页面还是会卡顿(这在一个维护时间很长的大应用中是很常见的)。这是因为React15的更新流程是同步执行的,一旦开始更新直到页面渲染前都不能中断。


三、从架构演变看不断进击的 React 都做过哪些优化?


React渲染页面的两个阶段


  • 调度阶段(reconciliation):在这个阶段 React 会更新数据生成新的 Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列


  • 渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到DOM上


React 15 架构


React15架构可以分为两层:


  • Reconciler(协调器)—— 负责找出变化的组件;


  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上;


在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。


为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。


React 16 架构


为了解决同步更新长时间占用线程导致页面卡顿的问题,也为了探索运行时优化的更多可能,React开始重构并一直持续至今。重构的目标是实现Concurrent Mode(并发模式)。


从v15到v16,React团队花了两年时间将源码架构中的Stack Reconciler重构为Fiber Reconciler。


React16架构可以分为三层:


  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler;


  • Reconciler(协调器)—— 负责找出变化的组件:更新工作从递归变成了可以中断的循环过程。Reconciler内部采用了Fiber的架构


  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上。


React 17 优化


React16的expirationTimes模型只能区分是否>=expirationTimes决定节点是否更新。React17的lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。


Lane用二进制位表示任务的优先级,方便优先级的计算(位运算),不同优先级占用不同位置的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的任务需要赋予什么优先级等问题都是Lane所要解决的问题。


Concurrent Mode的目的是实现一套可中断/恢复的更新机制。其由两部分组成:


  • 一套协程架构:Fiber Reconciler


  • 基于协程架构的启发式更新算法:控制协程架构工作方式的算法


四、浏览器一帧都会干些什么以及requestIdleCallback的启示


浏览器一帧都会干些什么?


我们都知道,页面的内容都是一帧一帧绘制出来的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面表现就也细腻。目前浏览器大多是 60Hz(60帧/s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms) 过程中浏览器又干了些什么呢?


6eb71c7b726c2e4592af7d229418645f.png


通过上面这张图可以清楚的知道,浏览器一帧会经过下面这几个过程:


  1. 接受输入事件


  1. 执行事件回调


  1. 开始一帧


  1. 执行 RAF (RequestAnimationFrame)


  1. 页面布局,样式计算


  1. 绘制渲染


  1. 执行 RIC (RequestIdelCallback)


第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。


requestIdleCallback 的启示


我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。


requestIdleCallback((deadline) => {
// deadline 有两个参数
  // timeRemaining(): 当前帧还剩下多少时间
  // didTimeout: 是否超时
// 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 则会强制浏览器在当前帧执行完后执行。
 if (deadline.timeRemaining() > 0) {
   // TODO
 } else {
  requestIdleCallback(otherTasks);
 }
});
// 用法示例
var tasksNum = 10000
requestIdleCallback(unImportWork)
function unImportWork(deadline) {
  while (deadline.timeRemaining() && tasksNum > 0) {
    console.log(`执行了 ${10000 - tasksNum + 1}个任务`)
    tasksNum--
  }
  if (tasksNum > 0) { // 在未来的帧中继续执行
    requestIdleCallback(unImportWork)
  }
}


其实部分浏览器已经实现了这个API,这就是requestIdleCallback。但是由于以下因素,Facebook 抛弃了 requestIdleCallback 的原生 API:


  • 浏览器兼容性;


  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低。


基于以上原因,在React中实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

相关文章
|
8月前
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍2
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍2
37 0
|
4月前
|
移动开发 前端开发 JavaScript
react fiber架构【详细讲解,看这一篇就够了】
react fiber架构【详细讲解,看这一篇就够了】
142 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍2
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍2
35 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍1
前端学习笔记202307学习笔记第五十七天-react源码-Fiber数据结构介绍1
45 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍
51 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
46 0
前端学习笔记202307学习笔记第六十天-react源码-子级节点Fiber对象的构建流程
|
8月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-构建多个子集fiber的情况
前端学习笔记202307学习笔记第六十天-react源码-构建多个子集fiber的情况
33 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-构建单个子集fiber的情况2
前端学习笔记202307学习笔记第六十天-react源码-构建单个子集fiber的情况2
30 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第六十天-react源码-构建单个子集fiber的情况1
前端学习笔记202307学习笔记第六十天-react源码-构建单个子集fiber的情况1
40 0
|
8月前
|
前端开发
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍2
前端学习笔记202307学习笔记第五十九天-react源码-Fiber数据结构介绍2
34 0