前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正(免责声明)
博客链接:pionpill
参考文献:
React 技术揭秘: kasong.gitee.io/just-react/…
React 之 Fiber 架构: juejin.cn/post/706715…
从这篇文章开始剖析 React16 之后的 Fiber 架构。本篇主要是一些框架设计理念。
如果了解 Java 一定知道多线程这个概念,Java 通过同一时间不同线程执行不同的逻辑实现了多任务处理,Java 的多任务处理基本单元是 thread。React 多任务处理的基本单元是 fiber。
- thread: 对应 CPU 中的线程,是一种抢占式多任务方式,是正真意义上的同一时刻执行多个任务。
- fiber: 是一种非抢占式多任务方式,是指在一个时间段内执行多个任务,由于 js 的限制并不能并行执行多任务。
通过比较新的浏览器 worker 线程可以实现真正的 JS 并行处理,但这不在我们这几篇文章的讨论范围内。
非抢占式也叫协作式,其特点是要求每一个运行中的程序,定时放弃自己的运行权限,告诉操作系统执行下一个程序。
Fiber 架构理念
目前有两种主流的方案解决长任务卡顿问题:
- 优化框架,使代码被浏览器更快地解析(废话)。
- 将长任务"分解"为多个短任务,将其分派在多个帧中执行。
Fiber 架构的理念就是第二种,让 JS "定时主动"放弃自己的运行权限。
React vs Vue:由于 react 大量采用了 JSX 语法,因此在解析过程中会多一步 JSX -> vDOM 的操作,这限制了使用第一种方案的优化上限(但是 JSX 用起来很爽!)。而 Vue 采用更接近原生 HTML 的写法,可以对模板代码做更极致的优化。
Fiber 的处理逻辑
只要我们将长任务划分为多个短任务,放在不同的浏览器帧时间内执行,就不会产生卡顿。Fiber 架构中,浏览器每一帧时间内,都会预留一些时间给 JS 线程(5ms,这段时间也被可以叫做时间片),react 利用这部分时间更新组件,剩余的时间留给浏览器其他任务。等到下一帧重新执行未完成的操作。
查看使用 Fiber 架构的堆栈图,会发现异步渲染时每帧时间执行完任务后,到达 16.6ms,任务会立即停止,待到下一帧开始新的任务。
这样的处理逻辑带来了一个特性: 操作的决定权由框架转移到了浏览器。在以往的处理逻辑中,都是框架控制 JS 线程,继而决定浏览器的操作(怎么执行,执行到哪结束,执行多久框架说了算)。Fiber 架构规定了仅能在一帧时间内进行操作,只有浏览器有空闲资源时才会让框架逻辑执行。
新老架构对比
React 15 及以前使用的是 Stack 架构,采用同步不可中断的架构操作虚拟 DOM。React 16 使用的是异步可中断的 Fiber 架构。Fiber 架构是基于 Stack 架构的,采用可中断递归的方案操作虚拟 DOM。
Stack 架构
React15 的 Stack 架构可以分为两层:
- Reconciler(协调器): 找出变化的组件
- Renderer(渲染器): 将变化的组件渲染到页面上
Stack Reconciler
在 React 中可以通过 setState forceUpdate render 等 API 触发更新。每当有更新时会执行如下工作:
- 调用类组件的
render方法或者函数组件的返回值,将 JSX 转化为 vDOM。 - 将新老 vDOM 对比。
- 找出变化的 vDOM。
- 通知 Renderer 将变化的 vDOM 渲染到页面上。
Stack Reconciler 在 mount 过程中会调用 mountComponent,update 过程会调用 updateComponent。这两个方法都会递归更新组件(是不可中断的)。Stack 架构与 Fiber 架构最大的区别是 Reconciler 变了,Fiber Reconciler 使得组件更新的过程可中断。
Renderer
渲染器负责在各种环境渲染DOM,React 除了浏览器平台还支持很多平台,常见的渲染器有:
- ReactDOM: 浏览器环境的 Renderer
- ReactNative 渲染器: App 环境渲染
- ReactArt 渲染器: Canvas, SVG 渲染
Stack 架构的缺陷
对于 React15 来说,协调器和渲染器是交互进行的,假设有这样一个 DOM 结构:
<div>a</div>
<div>
<div>b1</div>
<div>b2</div>
</div>
<div>c</div>
这里我们将所有字母替换为大写,那么会执行如下操作:
- Reconciler 发现 a->A, 通知 Renderer
- Renderer 更新 a->A
- Reconciler 发现 b1->B1, 通知 Renderer
- Renderer 更新 b1->B1
- Reconciler 发现 b2->B2, 通知 Renderer
- Renderer 更新 b2->B2
- Reconciler 发现 c->C, 通知 Renderer
- Renderer 更新 c->C
这个过程是递归的,如果存在子 DOM,会采用深度优先搜索算法替换。
我们之前说过,Stack Reconciler 有个缺陷: 同步不可中断。上述的 8 个步骤如果停在了任意一个步骤,用户的界面就会显示错误: 一个初始状态与预想状态的不完全 DOM。
此外,由于是采用递归的方式更新 DOM,在实际操作中,每个 DOM 更新都会执行如下具体的操作:
componentWillMount()render()componentDidMount()
注意,某一个 DOM 如果存在子 DOM,上面三个方法并不会全部执行完并返回,而是在 render() 之后停留,等待子 DOM 执行完后再调用 componentDidMount() 方法。如果 DOM 结构很复杂,例如 1w 个 DOM 需要递归,那就会有 9999 个 componentDidMount() 等待最后一个 DOM 操作执行完才能调用。
基于这些原因,react 团队决定改进框架。
Fiber 架构
Fiber 架构多了一层:
- Scheduler(调度器): 调度任务的优先级
- Reconciler(协调器): 找出变化的组件
- Renderer(渲染器): 将变化的组件渲染到页面上
Fiber 框架最核心的是 Fiber Reconciler。为了支持 Fiber Reconciler,重构出了 vDOM 的具体实现 FiberNode。
Scheduler
既然我们的浏览器每帧要取出任务执行,那肯定需要一个 Scheduler 来决定取哪个任务。
浏览器有一个原生的 requestIdleCallback 方法(即上一篇文章中的 idle),可以决定事件执行优先级,但是基于以下原因 react 没有采用这个方法:
- 兼容性问题,有的浏览器不支持这个 API。
- 这个方法任务执行优先级无法自定义,且并不符合 react 团队的预期
总之,react 最后自己封装了一个 Scheduler,提供更强大的事件优先级处理逻辑,取代了 idle 的功能。具体执行策略会在其他文章中讲解。
Fiber Reconciler
这里简单讲一下 Fiber Reconciler 的执行逻辑。在 React 16 之后,Reconciler 与 Renderer 不再是交替工作。Reconciler 会给 vDOM 打上增删改查的标记:
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
Scheduler 和 Reconciler 的工作都在内存中执行,只有所有组件完成 Reconciler 工作后,才会统一交给 Renderer。
react 源码结构
这里我们看的是 main 分支最新(当前为 v18.2.0)的源代码,如果后续 react 有大的更新导致项目结构变更,可以看 v18.2.0 标签的源码:
- main: github.com/facebook/re…
- v18.2.0: github.com/facebook/re…
我会在每次引用源代码前给上 Github 链接,以便实时查阅
react 核心源代码位于 packages 文件夹中,这里有如下重要的文件:
|- react // react 的核心,包含所有全局 API
|- schedular // 调度器文件夹
|- react-reconciler // 协调器,Diff 算法,最难啃的部分
|- shared // 全局共享配置
其他还包含一些 SSR,跨平台,测试模块。
后文默认读者对 React 有一定的基础。我会把 React 的几个核心模块的功能都讲一遍,贴上关键的源代码。
react 发展历程
我们可以将 react 发展历程分为两个阶段:
- React15-: 引入 JSX 语法,vDOM,Diff 状态更新等概念。直到 React15 都在此基础上完善功能;特点是类组件,同步渲染。
- React16-18: 推出 Fiber 架构,异步处理机制。特点是可中断任务,函数组件,钩子语法。
目前 react 还有两个主要发展方向:
- 一是在 Fiber 架构的基础上,完善异步处理机制。
- 二是往 SSR(服务端渲染) 方向发展。
后文说明
关于看 react 源代码的几点说明(吐槽):
- 源代码中有很多看似无效的内容,比如一个
if判断后没有进行任何处理,但是有注释,表示这里以后会加内容或者只是为了可读性。 - 源代码有很多远古写法(ES6 之前),以及一些为了兼容性放弃可读性的写法。
- 源代码包含了大量与核心逻辑无关的内容:开发模式,性能分析。
- Scheduler 代码的存在会影响我们看核心逻辑。
- 源代码虽然是 js 文件,但使用了 ts 语法
- 源代码中包含了很多开发者提示需要重构或者进一步处理的注释。
总而言之,react 源代码很难读(并不是逻辑上难,而是可读性不高),后文贴的源代码我会做一些改动:
- 无用的逻辑判断,直接省略。
- 使用 ES6 语法优化部分可读性差的代码片段。
- 对于开发模式(__DEV__),开启性能追踪(
enableTransitionTracing),开启性能分析计时器(enableProfilerTimer) 等非生产环境逻辑代码全部删除。 - Scheduler 相关代码如果删除,会给出注释
- 使用 ts 语法
大部分代码上面会贴源码地址,文中的代码是剔除我认为不重要的部分后优化过的
文字说明
一些概念与常用语:
- 帧时间: 浏览器执行 1 帧所需要的时间
- 时间片: react 在帧时间内可执行 js 代码的时间
- 冲刷(flush): 执行完某队列的所有任务