# React 理念
# react 理念
React 被设计用于 JavaScript 构建快速响应的大型 Web 应用程序。
由此可见,
快速响应是 react 框架的关键
# 难点限制
需要解决快速响应的难点就要解决以下两种限制:
CPU 的瓶颈
遇到大量计算操作时,页面掉帧导致卡顿
function App() {
const len = 3000;
return (
<ul>{new Map(len).fill(0).map((item, index) => <li>index</li>);
}</ul>)}这样当页面执行 js 代码时,由于渲染线程和 js 脚本执行互斥,所以页面迟迟没有渲染,造成卡顿
解决方案:
在浏览器每一帧的时间中,预留一些时间给 JS 线程,React 利用这部分时间更新组件(预留的初始时间是 5ms)。
预留的时间不够用时,react 将线程控制权交还给浏览器,使其有时间渲染 UI 界面。
这种将长任务拆分到每一帧中,每次执行一小段任务的操作成为时间切片
// 通过使用 ReactDOM.unstable_createRoot 开启 Concurrent Mode, 启用时间切片// ReactDOM.render(<App/>, rootEl);ReactDOM.unstable_createRoot(rootEl).render(<App />);
解决 CPU 瓶颈的关键在于实现时间切片,而时间切片的关键是:
将同步的更新变为可中断的异步更新。
IO 的瓶颈
发送网络请求后,需要等待数据返回才能进一步操作
网络延迟是无法解决的,如何在网络延迟客观存在的情况下,减少用于对网络延迟的感知?
如果请求数据和显示页面并行执行,就会造成页面的卡顿,而一旦先显示页面,后面请求数据再更新就会流畅很多。
react 采用的方案是:将界面 UI 的同步更新变为可中断的异步更新
# React15 架构
React15 的架构分为协调器和渲染器
# 协调器
- Reconciler 协调器负责找出变化的组件。
- React 中通过
this.setState、this.forceUpdate和ReactDOM.render等 API 进行更新。每当有更新发生,协调器就会做如下工作:- 调用函数组件或者类组件的
render方法,将返回的 JSX 转化为虚拟 DOM - 将虚拟 DOM 和上次更新时的虚拟 DOM 比对
- 通过比对找出本次更新中变化的虚拟 DOM
- 通知渲染器将变化的虚拟 DOM 渲染到页面上
- 调用函数组件或者类组件的
# 渲染器
- React 支持跨平台,所以不同平台有不同的 Renderer 渲染器,浏览器环境是
ReactDOM - Renderer 渲染器负责将变化的组件渲染到页面上
# 缺点
mount 的组件会同步递归更新子组件,造成 UI 卡顿。
在协调器 Reconciler 中,mount 的组件会调用 mountComponent,update 的组件会调用 updateComponent 方法,这两个方法都会递归更新子组件,而递归更新子组件是非常消耗性能的。
同步更新 UI 视图,界面卡顿
视图同步更新,并且 React15 不会中断进行中的更新
# React16 架构
React16 架构可以分为三层:
- Scheduler 调度器:负责调度任务的优先级,高优先级的任务先进入 Reconciler 进行协调
- Reconciler 协调器:找出变化的组件
- Renderer 渲染器:将变化的组件渲染到页面上
React16 架构相比 React15 添加了 Scheduler 调度器
# 调度器
首先以浏览器是否有剩余时间作为任务终端的标准,那么就需要一种机制,当浏览器有剩余时间时通知我们。可以使用
requestIdleCallback这个 API,但是因为下面的原因被弃用了:- 浏览器兼容性(跨平台所不能容忍的)
- 触发频率不稳定,会受很多因素影响
Scueduler 调度器库由此诞生,并且独立于 React
# 协调器
React15 中的 Reconciler 协调器是同步递归处理虚拟 DOM 的,这样会造成 UI 视图层的卡顿。在 React16 中将更新工作从递归变成了可以中断的循环过程,每次循环都会调用
shouldYield判断当前是否有剩余时间React16 解决中断更新时,DOM 渲染不完全的问题
React16 中,调度器和协调器不再是交替工作,当调度器将任务交给协调器之后,协调器会为变化的虚拟 DOM 打上增 / 删 / 改 的标记
整个工作都在内存中进行,只有当所有组件都完成协调的工作,才会统一交给渲染器进行渲染。
# 渲染器
Renderer 渲染器根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。
# 流程
- 例如:点击 button 按钮,触发更新
- 调度器受到更新,判断是否有凄然优先级更高的任务需要执行,如果没有就将组件状态改变,交给协调器
- 协调器接收到更新,将变化的虚拟 DOM 打上标记,交给渲染器
- 渲染器对打标记的 DOM 元素执行对应的操作
中间两个任务(调度器和协调器)随时都可能由于以下原因被中断:
有其他更高优先级的任务需要执行
当前帧没有剩余时间
由于调度器和协调器的工作都在内存中进行,不会更新到页面上,所以即使反复中断,对用户来说也无感
# Fiber 介绍
Fiber 其实是一个数据结构,需要记录节点和节点之间的关系,在 Fiber 架构中的任务是可以中断执行的,继续执行时会丢弃原来完成的工作从头再来(数据不可变的原因),并且即使重新执行中断的任务,对用户侧来说也是无感的,因为 Fiber 架构的视图更新是后缓冲区视图 替换 前缓冲区视图的过程。
顺便理解一下 componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个钩子被废弃的原因:因为 render 阶段是异步执行、可被中断的,再次回来的时候有可能会丢弃已经完成的工作从头再来,这样就可能会导致 Fiber 节点在 render 阶段重复调用componentWillMount这几个钩子,导致不符合开发者预期,并且当这几个钩子中存在副作用时,就会出现问题。
// Fiber 中关键字段 | |
{ | |
type, //DOM 类型 | |
key, //reconciler 在协调过程中,决定该 fiber 是否要复用 | |
child, // 组件 render 函数返回值 | |
sibling, | |
return, //return 属性指向父节点 | |
pendingProps, | |
memoizedProps, | |
pendingWorkPriority, | |
alternate, | |
... | |
} |
# Fiber 架构
Fiber 架构出现的原因:
由于 JS 单线程执行,并且 React15 版本的 reconciler 协调阶段只能一次执行完,不能被中途打断,这就导致:当更新渲染的执行任务较多时,协调阶段一次执行的时间花费就很长,这时就会出现掉帧、卡顿的现象。由此推出 Fiber 架构
Fiber 架构实现逻辑:
Fiber 架构借鉴生成器 (或者说协程) 的概念,将任务分给一个个的迭代器执行,迭代器完成执行任务的过程发生在浏览器空闲阶段,这样就不会阻塞 UI 的渲染。(由于 RequestIdleCallback 存在兼容性问题、并且在大量用户操作的时候不够稳定,因此使用 RequestAnimationFrame 和 disPatchEvent 消息队列来完成)
# 双缓冲更新机制
双缓冲更新机制就是:在视图需要进行更新时,通过在后台更新新的 fiber 树,如果遇到优先级更高的任务,就暂停当前的执行 (conscrent 模型) 任务,此时,即使更新到一半,对于用户侧来说也是无影响的,当整个新的 fiber 树构建完成之后,才会去用新的 fiber 树替换掉旧的 fiber 树.
# Fiber 架构心智模型
# 代数效应
- 代数效应是函数式编程中的概念,用于
将副作用和函数相分离。 - 代数效应能够将副作用从函数逻辑中分离,使函数关注点保持纯粹
- 代数效应最明显的应用就是 Hooks
- 代数效应和 Fiber 架构:
- Fiber(纤程)和 Generator(协程)可以理解为代数效应思想在 JS 中的体现。
# Fiber
- React Fiber 可以理解为:
- React 内部实现了一套状态更新机制,支持任务的不同优先级,可以中断和恢复,并且恢复后可以复用之前的中间状态。
- 其中每个任务更新单元就是 React Element 对应的 Fiber 节点。
# Fiber 原理
# 原因
- 在 React15 及以前,协调器采用递归的方式创建虚拟 DOM,递归过程是不能中断的,如果组件树嵌套很深就会占用线程很多时间,造成 UI 卡顿。
- React16 将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归虚拟 DOM 数据结构无法满足需要了,就有了全新的 Fiber 架构。
# 概念
- React16 之后,虚拟 DOM 在 React 中有个正式的称呼:Fiber
- Fiber 包含三层含义:
- Fiber 架构,React16 的 Reconciler 协调器基于 Fiber 节点实现,被称为 Fiber Reconciler
- Fiber 数据结构,作为静态的数据结构,每一个 Fiber 节点对应一个 React 组件,保存了该组件的类型、对应的 DOM 节点等信息
- Fiber 动态工作单元,每一个 Fiber 节点保存了本次更新中该组件改变的状态和要执行的工作
- Fiber 节点对应 DOM 节点、Fiber 节点构成的 Fiber 树对应 DOM 树。
- 组件
mount时,Reconciler协调器根据 JSX 描述的组件内容生成组件对应的Fiber节点。 - 组件
update时,Reconciler协调器将JSX与Fiber节点保存的数据对比(diff 算法),生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记。
- 组件
# 原理
- 双缓存
- 在内存中构建并直接替换的技术叫双缓存,目的是不会出现白屏闪烁情况。
React使用 “双缓存” 来完成Fiber树的构建与替换 —— 对应着DOM树的创建与更新- 在
React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。
# diff 算法
由于 Diff 操作本身也会带来性能损耗,React 文档中提到,即使在最前沿的算法中,将前后两棵树完全比对的算法的复杂程度为 O (n 3),其中 n 是树中元素的数量。
如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个开销实在是太过高昂。
为了降低算法复杂度, React 的 diff 会预设三个限制:
只对同级元素进行
Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。两个不同类型的元素会产生出不同的树。如果元素由
div变为p,React 会销毁div及其子孙节点,并新建p及其子孙节点。开发者可以通过
key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:
# 单节点 diff

- 当
key相同且type不同时,代表我们已经找到本次更新的p对应的上次的fiber,但是p与litype不同,不能复用。既然唯一的可能性已经不能复用,则剩下的fiber都没有机会了,所以都需要标记删除。 - 当
key不同时只代表遍历到的该fiber不能被p复用,后面还有兄弟fiber还没有遍历到。所以仅仅标记该fiber删除。
# 多节点 diff
Diff算法 的整体逻辑会经历两轮遍历:(对 Children 和 Fiber 进行遍历)
第一轮遍历:处理
更新的节点。- 如果 DOM 节点可以复用,比较下一个节点
- 不可复用分两种情况
key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。key相同type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历
- 遍历完,跳出第一轮遍历。
第二轮遍历:处理剩下的不属于
更新的节点,需要对第一轮遍历的结果分别讨论:newChildren与oldFiber同时遍历完那就是最理想的情况:只需在第一轮遍历进行组件
更新。此时Diff结束。#
newChildren没遍历完,oldFiber遍历完已有的
DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement。#
newChildren遍历完,oldFiber没遍历完意味着本次更新比之前的节点数量少,有节点被删除了。所以需要遍历剩下的
oldFiber,依次标记Deletion。#
newChildren与oldFiber都没遍历完这意味着有节点在这次更新中改变了位置。接下来需要处理移动的节点并标记节点的移动进行处理。
# 实现调度系统
// 手写一个最简版的 React 调度系统 | |
// 核心概念:lane 优先级、任务队列、时间切片、调度循环 | |
// ==================== 优先级定义 ==================== | |
const ImmediatePriority = 1; // 同步优先级(最高) | |
const UserBlockingPriority = 2; // 用户阻塞优先级 | |
const NormalPriority = 3; // 正常优先级 | |
const LowPriority = 4; // 低优先级 | |
const IdlePriority = 5; // 空闲优先级(最低) | |
const priorityTimeout = { | |
[ImmediatePriority]: -1, | |
[UserBlockingPriority]: 250, | |
[NormalPriority]: 5000, | |
[LowPriority]: 10000, | |
[IdlePriority]: 1000 * 60 * 60, // 1 小时 | |
}; | |
// ==================== 任务节点 ==================== | |
class SchedulerTask { | |
constructor(callback, priority) { | |
this.callback = callback; // 实际要执行的函数 | |
this.priority = priority; // 优先级 | |
this.expirationTime = 0; // 过期时间 | |
this.next = null; // 链表指针 | |
this.sortIndex = Infinity; // 排序优先级 | |
} | |
} | |
// ==================== 任务队列(最小堆) ==================== | |
class SchedulerMinHeap { | |
constructor() { | |
this.heap = []; | |
} | |
push(task) { | |
const heap = this.heap; | |
heap.push(task); | |
this._siftUp(heap.length - 1); | |
} | |
pop() { | |
const heap = this.heap; | |
if (heap.length === 0) return null; | |
const first = heap[0]; | |
const last = heap.pop(); | |
if (heap.length > 0) { | |
heap[0] = last; | |
this._siftDown(0); | |
} | |
return first; | |
} | |
peek() { | |
return this.heap[0] || null; | |
} | |
get size() { | |
return this.heap.length; | |
} | |
_siftUp(idx) { | |
const heap = this.heap; | |
while (idx > 0) { | |
const parent = (idx - 1) >> 1; | |
if (heap[parent].expirationTime > heap[idx].expirationTime) { | |
[heap[parent], heap[idx]] = [heap[idx], heap[parent]]; | |
idx = parent; | |
} else { | |
break; | |
} | |
} | |
} | |
_siftDown(idx) { | |
const heap = this.heap; | |
const length = heap.length; | |
while (true) { | |
const left = 2 * idx + 1; | |
const right = 2 * idx + 2; | |
let smallest = idx; | |
if (left < length && heap[left].expirationTime < heap[smallest].expirationTime) { | |
smallest = left; | |
} | |
if (right < length && heap[right].expirationTime < heap[smallest].expirationTime) { | |
smallest = right; | |
} | |
if (smallest !== idx) { | |
[heap[smallest], heap[idx]] = [heap[idx], heap[smallest]]; | |
idx = smallest; | |
} else { | |
break; | |
} | |
} | |
} | |
} | |
// ==================== 调度器核心 ==================== | |
class Scheduler { | |
constructor() { | |
// 任务队列(按过期时间排序的小顶堆) | |
this.taskQueue = new SchedulerMinHeap(); | |
// 当前正在执行的任务 | |
this.currentTask = null; | |
// 是否正在调度 | |
this.isScheduling = false; | |
// MessageChannel 用于实现时间切片(兼容 requestIdleCallback) | |
this._channel = null; | |
this._initChannel(); | |
// 帧预算(5ms,用于时间切片) | |
this.frameDeadline = 0; | |
this._frameInterval = 5; // 5ms | |
// 当前时间(虚拟,用于计算过期时间) | |
this.currentEventTime = 0; | |
} | |
// 初始化 MessageChannel | |
_initChannel() { | |
this._channel = new MessageChannel(); | |
this._channel.port2.onmessage = () => this._flushWork(); | |
} | |
// 获取当前时间(毫秒) | |
_getCurrentTime() { | |
return performance.now(); | |
} | |
// 计算过期时间 | |
_computeExpirationTime(priority) { | |
const startTime = this._getCurrentTime(); | |
const timeout = priorityTimeout[priority]; | |
if (timeout === -1) { | |
return -1; // 同步任务立即过期 | |
} | |
return startTime + timeout; | |
} | |
// 检查是否应该让出主线程 | |
_shouldYield() { | |
const currentTime = this._getCurrentTime(); | |
return currentTime >= this.frameDeadline; | |
} | |
// 请求调度 | |
_request调度() { | |
// 使用 MessageChannel 发起调度 | |
this._channel.port1.postMessage(null); | |
} | |
// 执行单个任务 | |
_executeTask(task) { | |
const callback = task.callback; | |
if (!callback) { | |
// 任务已被取消,跳过 | |
return; | |
} | |
this.currentTask = task; | |
const finished = callback(); | |
if (typeof finished === 'function') { | |
// 返回了新 callback,继续调度 | |
task.callback = finished; | |
this.taskQueue.push(task); | |
} else { | |
// 任务完成,释放 | |
this.currentTask = null; | |
} | |
} | |
// 调度循环(核心) | |
_flushWork() { | |
this.isScheduling = true; | |
// 如果有 requestIdleCallback,用它来计算帧预算 | |
if (typeof requestIdleCallback !== 'undefined') { | |
requestIdleCallback((deadline) => { | |
this.frameDeadline = deadline.timeRemaining(); | |
this._workLoop(); | |
this.isScheduling = false; | |
this._checkForMoreWork(); | |
}); | |
} else { | |
// 兜底:手动计算帧预算 | |
this.frameDeadline = this._getCurrentTime() + this._frameInterval; | |
this._workLoop(); | |
this.isScheduling = false; | |
this._checkForMoreWork(); | |
} | |
} | |
// 工作循环 | |
_workLoop() { | |
while (true) { | |
// 获取下一个要执行的任务 | |
const task = this.taskQueue.peek(); | |
if (!task) break; | |
// 检查是否过期 | |
const currentTime = this._getCurrentTime(); | |
if (task.expirationTime <= currentTime && task.priority !== ImmediatePriority) { | |
// 任务过期,弹出并丢弃(实际 React 中会直接执行,这里简化处理) | |
this.taskQueue.pop(); | |
this._executeTask(task); | |
continue; | |
} | |
// 检查是否应该让出 | |
if (this._shouldYield()) { | |
// 让出主线程,等下一帧继续 | |
this._request调度(); | |
break; | |
} | |
// 执行任务 | |
this.taskQueue.pop(); | |
this._executeTask(task); | |
} | |
} | |
// 检查是否还有待处理的任务 | |
_checkForMoreWork() { | |
if (this.taskQueue.size > 0 && !this.isScheduling) { | |
this._request调度(); | |
} | |
} | |
// ==================== 对外 API ==================== | |
// 调度一个回调函数 | |
scheduleCallback(callback, priority = NormalPriority) { | |
const currentTime = this._getCurrentTime(); | |
this.currentEventTime = currentTime; | |
const task = new SchedulerTask(callback, priority); | |
task.expirationTime = this._computeExpirationTime(priority); | |
this.taskQueue.push(task); | |
// 如果是同步优先级,立即执行 | |
if (priority === ImmediatePriority) { | |
this._flushWork(); | |
} else { | |
// 请求调度 | |
this._request调度(); | |
} | |
return task; | |
} | |
// 同步刷新(React 的 flushSync) | |
flushSync(callback) { | |
// 临时提高优先级 | |
const prevScheduling = this.isScheduling; | |
this.isScheduling = true; | |
try { | |
return this.scheduleCallback(callback, ImmediatePriority); | |
} finally { | |
this.isScheduling = prevScheduling; | |
} | |
} | |
// 让出检查(React 的 shouldYield) | |
shouldYield() { | |
return this._shouldYield(); | |
} | |
// 取消任务 | |
cancelCallback(task) { | |
task.callback = null; | |
} | |
// 获取当前任务 | |
getCurrentTask() { | |
return this.currentTask; | |
} | |
} | |
// ==================== 创建全局调度器实例 ==================== | |
const scheduler = new Scheduler(); | |
// ==================== 辅助函数 ==================== | |
function getNextPriority() { | |
const task = scheduler.taskQueue.peek(); | |
return task ? task.priority : null; | |
} | |
// ==================== 示例 & 测试 ==================== | |
console.log('=== React 调度系统 测试 ==='); | |
// 测试 1: 正常优先级任务 | |
scheduler.scheduleCallback(() => { | |
console.log('[Normal] 任务1执行'); | |
}, NormalPriority); | |
// 测试 2: 低优先级任务 | |
scheduler.scheduleCallback(() => { | |
console.log('[Low] 任务2执行'); | |
}, LowPriority); | |
// 测试 3: 同步任务 | |
scheduler.scheduleCallback(() => { | |
console.log('[Immediate] 同步任务执行'); | |
}, ImmediatePriority); | |
// 测试 4: 模拟长时间任务(带时间切片) | |
let count = 0; | |
scheduler.scheduleCallback(() => { | |
function doWork() { | |
while (count < 10000000) { | |
count++; | |
if (count % 1000000 === 0) { | |
console.log(`[Normal] 进度: ${count / 1000000}/10`); | |
} | |
// 模拟时间切片检查 | |
if (scheduler.shouldYield()) { | |
// 让出,等下一帧继续 | |
scheduler.scheduleCallback(doWork, NormalPriority); | |
return; | |
} | |
} | |
console.log('[Normal] 长时间任务完成!'); | |
} | |
doWork(); | |
}, NormalPriority); | |
// 测试 5: flushSync | |
scheduler.flushSync(() => { | |
console.log('[flushSync] 同步执行的任务'); | |
}); | |
// 测试 6: 任务取消 | |
const taskToCancel = scheduler.scheduleCallback(() => { | |
console.log('这不应该被执行'); | |
}, LowPriority); | |
scheduler.cancelCallback(taskToCancel); | |
console.log('[Cancel] 已取消任务'); | |
// 测试 7: 优先级队列验证 | |
console.log('\n=== 队列顺序测试 ==='); | |
scheduler.scheduleCallback(() => console.log('[Immediate] A'), ImmediatePriority); | |
scheduler.scheduleCallback(() => console.log('[Normal] B'), NormalPriority); | |
scheduler.scheduleCallback(() => console.log('[Low] C'), LowPriority); | |
scheduler.scheduleCallback(() => console.log('[UserBlocking] D'), UserBlockingPriority); |
# 实现 useState
let workInProgressHook; | |
let isMount = true; | |
const fiber = { | |
memoizedState: null, | |
stateNode: App, | |
}; | |
function schedule() { | |
workInProgressHook = fiber.memoizedState; | |
const app = fiber.stateNode(); | |
isMount = false; | |
return app; | |
} | |
function dispatchAction(queue, action) { | |
const update = { | |
action, | |
next: null, | |
}; | |
if (queue.pending === null) { | |
update.next = update; | |
} else { | |
update.next = queue.pending.next; | |
queue.pending.next = update; | |
} | |
queue.pending = update; | |
schedule(); | |
} | |
function useState(initialState) { | |
let hook; | |
if (isMount) { | |
hook = { | |
queue: { | |
pending: null, | |
}, | |
memoizedState: initialState, | |
next: null, | |
}; | |
if (!fiber.memoizedState) { | |
fiber.memoizedState = hook; | |
} else { | |
workInProgressHook.next = hook; | |
} | |
workInProgressHook = hook; | |
} else { | |
hook = workInProgressHook; | |
workInProgressHook = workInProgressHook.next; | |
} | |
let baseState = hook.memoizedState; | |
if (hook.queue.pending) { | |
let firstUpdate = hook.queue.pending.next; | |
do { | |
const action = firstUpdate.action; | |
baseState = action(baseState); | |
firstUpdate = firstUpdate.next; | |
} while (firstUpdate !== hook.queue.pending); | |
hook.queue.pending = null; | |
} | |
hook.memoizedState = baseState; | |
return [baseState, dispatchAction.bind(null, hook.queue)]; | |
} | |
function App() { | |
const [num, updateNum] = useState(0); | |
console.log(`${isMount ? "mount" : "update"} num: `, num); | |
return { | |
click() { | |
updateNum((num) => num + 1); | |
}, | |
}; | |
} | |
window.app = schedule(); |
# 资源文档
React 技术揭秘