一、 虚拟 DOM
1. 定义
在传统页面的开发模式中,每次需要更新页面时都需要手动操作 DOM 来进行更新,频繁操作 DOM 造成浏览器性能消耗过大。

React 把真实 DOM 树转换为 JavaScript 对象树,即 Virtual DOM。Virtual DOM 把真实的网页文档节点,虚拟成一个个的 js对象,并以树型结构,保存在内存中。
Virtual DOM 实际上是在浏览器端用 javaScript 实现的一套 DOM API,包括所有 Virtual DOM 标签、生命周期的维护和管理、diff 算法、更新的 Patch 方法。

Virtual DOM 可以看作 fiber 节点
2. 更新流程
1. 组件挂载阶段
React 会结合 JSX 的描述构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射。
2. 组件更新阶段
每次数据更新时,重新计算 Virtual DOM ,并和上一次生成的 Virtual DOM 做对比,对发生变化的部分做批量更新。
React也提供了直观的 shouldComponentUpdate 生命周期回调,来减少数据变化后不必要的 Virtual DOM 对比过程。
通过 JS 模拟网页文档节点,生成 JS 对象树(虚拟 DOM),然后再进一步生成真实的 DOM 树,再绘制到屏幕。如果有内容发生改变,React 会重新生成一棵新的虚拟 DOM 树,与前面的虚拟 DOM 树进行比对 diff,把差异的部分打包成 patch,再应用到真实 DOM 中,然后渲染到浏览器屏幕上。

3. 优缺点
1. 优点
-
研发体验和研发效率:开发者无需在手动操作原生 DOM,即可实现数据驱动视图的更新
-
跨平台兼容性问题:将真实 DOM 转化为一套虚拟 DOM,即可支持不同终端,降低成本
-
批量更新:通过 batch 函数实现批量的更新。batch 函数的作用是缓存每次生成的补丁集,并暂存在队列中,并在最后一次性完成所有更新

2. 缺点
-
额外的内存使用:虚拟 DOM 需要更多的内存来存储虚拟 DOM 树
-
额外的计算开销:每次状态更新时,React 都需要计算虚拟 DOM 树的 diff 差异,这可能会占用CPU时间
-
额外的渲染时间:虽然虚拟 DOM 最终会被转换为真实 DOM 更新,但这可能会导致渲染时间的延长
虚拟 DOM 不适用以下场景中:需要减少 CPU 占用、DOM 更新频繁、DOM 操作简单、需要操作真实 DOM等
二、 调和 与 diff
1. 定义
虚拟 DOM 是一种编程概念,在这个概念里,UI 以一种理想化的形式存在于内存中,并通过 ReactDOM 等类库使之与真实 DOM 同步,这一同步过程叫做调和。
对比前后虚拟 DOM 树,找到所有差异的过程叫做 diff。
调和指的是将虚拟 DOM 映射到真实 DOM 的过程。调和过程并不能和 diff 画等号,调和是“使一致”的过程,而 diff 是“找不同”的过程,它只是“使一致”过程中的一个环节。
React 根据 diff 实现形式的不同,调和过程被划分为了以 React 15 为代表的“栈调和”以及 React 16 的“Fiber 调和”。
2. diff 算法
传统 diff:找出两个树结构之间的不同,需要进行遍历递归对树节点之间进行一一对比,时间复杂度为O(n^3)。
改良 diff:在原有思想的前提下,提出了三个新的原则
- Diff 算法性能突破的关键点在于“分层对比”
- 类型一致的节点才有继续 Diff 的必要性
- key 属性的设置,可以帮我们尽可能重用同一层级内的节点
const todoItem = todos.map(item =>{
return <div key={item.id}>{item.text}</div>
})
官方对 key 属性的定义如下:key 是帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果 key 值发生了变更,React 会触发 UI 的重渲染。所以这是一个非常有用的特性。
3. diff 算法实现
1. tree diff
方式:分层比较,两棵树只会对同一层的节点进行比较
React 通过 updateDepth 对虚拟 DOM 树进行层级控制,只会对相同层级的 DOM 节点进行比较。当发现节点已经不存在时,该节点及其所有子节点会被完全删除,不会用于进一步的比较。从而实现一次遍历,便能完成整棵DOM树的比较。
跨节点移动并不是执行一个移动操作,而是执行创建、删除的操作,会以被移动节点为根节点的整棵树的重新创建,及其损耗性能。
当 R 发现 A 消失了,则会销毁 A;当 D 发现多了一个节点 A,会创建 A 及其子节点 B、C。此时,diff 的执行情况为 creat A -> creat B -> creat C -> delete A

2. component diff
-
不同类型组件:直接删除和创建组件下的所有子节点
-
相同类型组件:对于同一类型的组件,有可能虚拟 DOM 并没有改变,继续比较反而浪费 CPU 和时间,React 允许在 shouldComponentUpdate 中判断该组件是否需要进行 diff 算法分析,或者通过 useMemo、useCallback、memo 缓存结果
3. element diff
当节点处于同一层级时,diff 提供了三种节点操作,分别是插入、移动和删除,可以通过唯一标识符 key 进行判断和操作。
插入:新的组件类型不在旧集合里,是一个全新的节点,需要对新节点执行插入操作。
移动:旧集合里有新组件类型,且 element 是可更新的类型,通过移动操作实现 DOM 节点复用。
删除:旧组件类型不在新集合中,或者虽然在新集合中存在,但 element 不能直接复用和更新,需要删除 DOM 节点。
React 首先对新集合进行遍历 for( name in nextChildren),通过唯一 key 值判断新旧集合中是否存在相同的节点 if (preChild === nextChild ),如果不存在则创建节点,如果存在则进行移动操作,移动前会判断下标 if(child_mountIndex < lastIndex)。节点的操作过程中会不断的更新 lastIndex,lastIndex 记录已更新的当前节点的下标。
React 完成新集合中所有节点的差异化对比后,还需要对旧集合进行遍历,删除新集合中没有的旧节点。

4. Patch 方法
React 通过 patch 将 diff 计算出来的 DOM 差异队列更新到真实 DOM 节点上,最终让浏览器渲染出更新的数据。
patch 会遍历差异队列,进行更新操作,包括新节点的插入、已有节点的移动和删除。
React 完成所有差异计算,并全部放入差异队列后,才开始执行 patch 方法,完成真实 DOM 的更新。
diff 算法分析阶段,添加差异节点到差异队列时是有序添加,所以 patch 时直接按照 index 操作真实 DOM
processUpdate:function(parentNode,updates){
for(var k = 0; k < updates.length;k++){
var update = updates[k];
switch(update.type){
// 插入
case ReactMultiChildUpdateTypes.INSERT_MARKUP:
// 移动
case ReactMultiChildUpdateTypes.MOVE_EXISTING:
// 删除
case ReactMultiChildUpdateTypes.REMOVE_NODE:
}
}
}
三、 React 更新模式衍变
- legacy 模式:
ReactDOM.render(<App />, rootNode),当前 React app 使用的就是这个模式 - blocking 模式:
ReactDOM.createBlockingRoot(rootNode).render(<App />),迁移到 concurrent 模式的第一个步骤 - concurrent 模式:
ReactDOM.createRoot(rootNode).render(<App />),这个模式开启了所有的新功能
1. legacy 模式
legacy 模式是常用的,它构建 dom 的过程是同步的,并且认为任务的优先级是相同的。所以在 render 的reconciler 中,如果 diff 的过程特别耗时,那么导致的结果就是 js 一直阻塞高优先级的任务(例如用户的点击事件),表现为页面的卡顿,无法响应。
legacy 模式的更新默认是异步批量更新,可以通过setTimeout、setInterval将其转换为原生事件系统中的事件执行模式
// 默认情况
const handleClick = ()=>{
setName(name+'lily');
setCount(count+1);
}
useEffect(()=>{
console.log('更新render'); // 只打印一次
})
// 立即执行
const handleClick = ()=>{
setTimeout(()=>{
setName(name+'lily');
setCount(count+1);
})
}
useEffect(()=>{
console.log('更新render'); // 打印两次
})
2. concurrent 模式
concurrent 模式用时间切片调度实现异步可中断的任务。根据设备性能的不同,时间切片的长度也不一样,在每个时间切片中,如果任务到了过期时间,就会主动让出线程给高优先级的任务。
concurrent 模式的自动批量处理对 setTimeout、setInterval 做了优化,自动批量处理采用异步任务统一开启更新调度。不再依赖事件系统,异步条件下也可以实现批量更新。
时间切片 + 自动批量更新 + 让出浏览器主动权
const handleClick = ()=>{
setTimeout(()=>{
setName(name+'lily');
setCount(count+1);
})
}
useEffect(()=>{
console.log('更新render'); // 打印一次
})
四、 React 调和衍变
1. Stack Reconciler
React官方认为,React是用JavaScript构建快速响应的大型Web应用程序的首选方式。
多线程的浏览器除了要处理JavaScript线程以外,还需要处理各种各样的任务线程,如处理DOM的UI渲染线程等。由于JavaScript线程也是可以操作DOM的,所以这两个线程在运行时是相互排斥的,即当其中一个线程执行时,另一个线程只能挂起等待。
如果JS线程执行长任务,则会导致渲染线程一直处于等待状态,界面就会长时间得不到更新,带给用户的体验就是所谓的“卡顿”。
React15的栈调和机制下的diff算法其实是树深度优先遍历的过程。Reconciler调和器会重复“父组件调用子组件”的过程直到最深的一层节点更新完毕,才慢慢向上返回。
Stack Reconciler过程的致命性问题在于其是同步的,不可以被打断,所以需要的调和时间会很长,导致JavaScript线程长时间地霸占主线程,进而导致上文中所描述的渲染卡顿/卡死、交互长时间无响应等问题。

2. Fiber Reconciler
异步可中断的渲染+任务切片+优先级控制+优先级调度
异步可中断的渲染:Fiber 通过任务切片将渲染工作分成一个个小任务,使得渲染过程可以在必要时被中断,从而让浏览器有机会处理用户输入等高优先级的任务
优先级控制:Fiber 允许为不同类型的更新分配不同的优先级。通过这种机制,React 可以确保高优先级的任务(如用户输入)能够快速得到响应,而不会被低优先级的任务(如动画或网络请求)所阻塞
优先级调度:Fiber 的调度器会根据任务的类型和优先级来调度任务,确保高优先级任务能够尽快得到处理。这是通过 React 内部的“优先级队列”实现的。
五、 fiber 架构
1. 定义
Fiber 本质是一个 JavaScript 对象,代表 React 的一个工作单元,包含了与当前组件相关的信息。
{
type: 'h1', // 组件类型
key: null, // React key
props: { ... }, // 输入的props
state: { ... }, // 组件的state (如果是class组件或带有state的function组件)
child: Fiber | null, // 第一个子元素的Fiber
sibling: Fiber | null, // 下一个兄弟元素的Fiber
return: Fiber | null, // 父元素的Fiber
// ...其他属性
}
当 React 开始工作时,会沿着 Fiber 树形结构进行,完成每个 Fiber 的工作(比较新旧 props,确定是否需要更新组件等)。如果主线程有更重要的工作发生(响应用户输入等),React 会中断当前工作并返回去执行主线程上的任务。
Fiber 架构中每个 fiber 都代表了一个工作单元,React 可以在处理任何 fiber 之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。
Fiber 不仅仅是代表组件的一个内部对象,还是 React 调度和更新机制的核心组成
2. fiber 节点
React 和 Vue 框架都是通过改变 VDOM 来实现真实 DOM 的更新,而 fiber 作为 React 中最小粒度的执行单元,所以可以将其理解成 VDOM。
每一个 Element 类型都会有一个与之对应的 fiber 类型,当 Element 发生变化引起组件更新时,会通过 fiber 层面做一次调和和改变,形成新的 DOM 做视图渲染,所以可以将 fiber 理解为 Element 和真实 DOM 之间的交流枢纽站。
fiber 的出现意在对渲染过程实现更加精细的控制
type Fiber = {
------- DOM实例 -----------
tag, // 标记不同的组件类型
type, // 组件类型
stateNode, // 实例对象
------- 构建Fiber树 -----------
return, // 指向父节点
child, // 指向子节点
sibling, // 指向第一个兄弟节点
alternate, // 当前Fiber在workInProgress中对应的Fiber
------- 状态数据 -------------
pendingProps, // 即将更新的props
memoizedProps, // 旧的props
memoizedState, // 旧的state
-------- 副作用 --------------
updateQueue, // 状态更新的队列
effectTag, // 将要执行的DOM操作
firstEffect, // 子树中第一个
lastEffect, // 子树中最后一个
nextEffect, // 链表中下一个
expirationTime, // 过期时间
mode, // 当前组件及子组件的渲染模式
}
3. fiber 树
每一个 fiber 节点都包含三个重要的属性:return、child、sibling
其中 return 指向父级 fiber 节点,child 指向子级 fiber 节点,sibling 指向兄弟 fiber 节点。

组件初次挂载的时候创建 fiber 树。
beginWork 函数
第一步:递归循环创建新的 fiber 节点

第二步:通过 child、return、sibling 属性建立 fiber 节点间的关系

completeWork 函数
自底向上执行 completeWork 方法,处理 fiber 节点到 DOM 的映射逻辑。
beginWork 递归无法进行时,则会执行 completeWork
-
创建DOM节点,赋值给 workInProgress(内存中的树)的 stateNode 属性
-
通过 appendAllChildren 函数将 DOM 节点插入 DOM 树中(子 fiber 节点对应的 DOM 节点挂载到父 fiber 节点对应的 DOM 节点中)
-
为 DOM 节点设置属性
处理副作用链 effectList
副作用链可以理解为 render 阶段“工作成果”的一个集合,每一个 Fiber 节点都维护了一个独有的 effectList,effectList 不只记录当前需要更新的节点,还记录了后代节点信息等。
把所有需要更新的 Fiber 节点单独串成一串链表,方便后续有针对性地对它们进行更新。这就是所谓的“收集副作用”的过程。
- firstEffect:链表的第一个Fiber节点
- lastEffect:链表的最后一个Fiber节点

4. fiber 树渲染流程
**createFiberRoot **
当通过 ReactDOM.render 创建应用时,会执行 createFiberRoot 方法。
function createFiberRoot(containerInfo, tag) {
// 生成 fiberRoot。containerInfo 是挂载根节点(比如 div#root)
const root = new FiberRootNode(containerInfo, tag);
// 生成 rootFiber
const uninitializedFiber = createHostRootFiber(tag);
// 互相关联
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
return root;
}
createFiberRoot 创建一个了 fiberRoot,以及一个 rootFiber。
一个 fiberRoot 是 fiber 树的根节点的维护者,是 fiberRootNode 实例,通过 current 确定要使用哪一棵 fiber 树。
每调用 createRoot 都会构建新的 fiber 树,并生成一个新的 fiberRoot 去指向它。
rootFiber 是一颗 fiber 树的根节点,也属于是 fiber 节点。rootFiber 通过属性 stateNode 访问到 fiberRoot。
performUnitOfWork
调和阶段,主要分为 beginWork 和 completeUnitOfWork 两部分。
-
beginWork 自上而下,进行新旧节点对比,构造子 fiber,并打上 flag(插入、更新、删除),会执行 render(生成新 ReactElement) 之前的生命周期函数。对应以前 stack reconciler 架构中递归的 “递”。
-
completeUnitOfWork 自下而上,如果是插入,则构建真实 DOM 节点放到 fiber.stateNode 下,接着是处理 props,将属性添加到 DOM 上。
5. render 阶段
React15 栈调和
React15的调和过程是一个递归的过程,ReactDOM.render 触发的同步模式下仍然是一个深度优先搜索的过程。
在这个过程中,beginWork 将创建新的 Fiber 节点,completeWork 则负责将 Fiber 节点映射为 DOM 节点。
React16 Fiber调和
React 双缓冲树原理:
React 底层会同时构建两颗树:一颗 workInProgress(在内存中创建),一颗为Current(渲染树),两颗树之间通过一个 alternate 指针相互指向。

当 React 项目初始化的时候,两棵树是相同的;当 React 项目更新时,所有的更新都发生在 workInProgress 上面,更新结束时,workInProgress 的状态是最新的,它将变成 Current 树,用于渲染视图。
current 树与 worklnProgress 树可以对标“双缓冲”模式下的两套缓冲数据,当 current 树呈现在用户眼前时,所有的更新都会由 worklnProgress 树来承接。workInProgress 树将会在用户看不到的地方(内存里)悄悄地完成所有改变,直到 current 指针指向 workInProgress 树时,用户可以看到更新后的页面。
由于双缓冲模式是合并后才被应用,所以可能会隐藏某些状态更新的问题,可以使用 flashSync、useLayoutEffect 处理等。
6. commit 阶段
commit 阶段主要处理带有 flags 的 fiber,patch 操作真实 DOM ,执行生命周期等。
commit 阶段分为以下三个小阶段:
- before mutation:DOM 节点还没有被染到界面上去
- mutation:负责 DOM 节点的渲染
- layout:处理 DOM 渲染完毕之后的收尾逻辑,以及把 fiberRoot 的 current 指针指向 worklnProgress Fiber 树等
7. fiber 架构优点
React V15以及之前的版本中,对于 VDOM 采用的是递归遍历的方式进行更新,比如一次更新会从应用的根部开始递归,而且一旦开始,中间不能中断,如果项目很大,则会造成浏览器卡顿甚至更严重的性能问题。

React V16引入的 fiber 架构,之所以能够解决卡顿问题,是因为其更新过程的Reconciler 调和器的作用。在 React 中每一个 fiber 都可以作为一个执行单元来处理,更新时会判断 fiber 是否需要更新(V17之前通过 expiration 过期时间,V17之后通过 lane 架构),浏览器是否还有空间和时间来执行更新。如果判断结果是不更新,则是直接跳过;如果没有时间更新,就把主动权交给浏览器去执行渲染、绘制等任务。等到浏览器有空余时间,再通过 scheduler(调度器)再次恢复渲染,从而提高用户体验。

8. fiber 架构特点
-
增量渲染:将更新任务拆分成多个小任务单元(称为 “fiber”),并使用优先级调度器来处理这些任务
-
优先级调度:根据任务的优先级来决定任务的执行顺序,确保高优先级任务得到及时处理
-
中断与恢复:可以在渲染过程中中断任务,然后在适当的时机恢复执行,从而避免了阻塞的情况
-
任务取消:可以取消不必要的更新
9. 架构优化对比
React15的更新渲染流程:

React16的更新渲染流程:

React 在 render 阶段将一个庞大的更新任务,拆解为若干个小的更新工作单元,每一个单元都被设置了一个不同的优先级。React 根据优先的高低,实现工作单元的打断和恢复等,从而完成整个更新任务。
正因为 Fiber 架构有如上的更新,所以需要废除 componentWillXXX 的生命周期。
六、scheduler 任务调度
1. 任务池
React 中的 fiber 任务优先级的不同,所以创建了两个任务池进行存储。
var taskQueue = []; // 立即执行
var timerQueue = []; // 延迟执行
React 中每一个 fiber 任务形式如下所示:
var newTask = {
id: taskIdCounter++, // 标记任务id
callback, // 回调函数
priorityLevel, // 任务优先级
startTime, // 任务开始时间,时间点
expirationTime, // 过期时间,时间点
sortIndex: -1, // 任务排序,取值来自过期时间,值越小优先级越高
};
React 中一旦产生新任务,就会先用 currentTime 记录当前时间(performance.now()或者Date.now()),如果任务有 delay 参数,那么任务开始执行时间 startTime = currentTime + delay;。接下来通过 startTime > currentTime 判断,如果返回 true,证明任务是可以延期的,那么任务进入 timerQueue,否则进入 taskQueue。
2. 小顶堆数据结构
React 利用小顶堆维护任务池,把 taskQueue 做成最小堆的数据结构,然后执行任务的时候,取最小堆的最小任务,如果任务执行完毕,就把这个任务从 taskQueue 中删除,并重新调整剩下的任务池。
当 taskQueue 中插入新任务的时,同时调整 taskQueue ,保证新的任务池仍然是最小堆。

3. scheduler 核心原理
浏览器的 EventLoop
一次渲染过程,按 60帧计算,大概有 16.6ms,在这个过程中浏览器的工作包括 “执行 JS -> 空闲 -> 绘制(16ms)”。
执行 JS表示的是浏览器的 JS 线程执行 eventloop 的过程,包括宏任务和微任务的执行,执行宏任务的数量是由浏览器决定。
如果某一个宏任务及其后执行微任务时间太长,都会延后浏览器的绘制操作
React 的 Scheduler
React 为了解决 15 版本存在的问题:组件的更新是递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
React 引入了 Fiber 的架构,同时配合 Schedduler 的任务调度器,在 Concurrent 模式下可以将 React 的组件更新任务变成可中断、恢复的执行,就减少了组件更新所造成的页面卡顿。
每一个封信任务都会赋予一个优先级,当更新任务抵达调度器时,高优先级的任务会优先进入 Reconciler 层。(设置优先级)
此时如果有新的更新任务抵达调度器,调度器会比较其优先级,若发现 B 的优先级高于当前任务 A,那么当前处于 Reconciler 层的 A 任务就会被中断。将更高优先级的 B 任务推入 Reconciler 层。当 B 任务执行完毕后,就会进入下一轮的任务调度。(可中断)
之前被中断的 A 任务会被重新推入 Reconciler 层,继续 A 任务的渲染流程。(可恢复)
Scheduler是一个独立的包,不仅仅在React中可以使用,其核心是时间切片、任务中断、任务恢复
1. !shouldYield() 时间切片
function workLoopConcurrent() {
while(workInProgress !== null && !shouldYield()){
performUnitOfwork(workInProgress);
}
}
当 shouldYield() 调用返回为 true 时,则说明当前需要对主线程进行让出。此时 whille 循环的判断条件整体为 false,while 循环将不再继续执行。
时间切片表示的是到时间点就停止,到时间点就停止... ...以此循环,最终看到的结果便是按一定时间段切割的效果
React 会根据浏览器的帧率计算时间切片的大小,并结合当前时间,计算出每一个切片的到期时间。在 workLoopConcurrent 函数中,每次执行都会判断当前切片是否到期,如果到期则让出主线程的使用权。


获取浏览器当前状态
requsetIdleCallback 是谷歌浏览器提供的一个 API,在浏览器有空余时间时会调用 requsetIdleCallback 的回调。
requsetIdleCallback(callback,{timeout});
- callback:回调函数
- timeout:超时时间
React 底层会把任务分为若干个小任务,每次只执行一个小任务,执行完会去请求浏览器的空闲帧,不仅可以有序执行还不会阻塞浏览器渲染。
React Scheduler 通过 requsetIdleCallback 向浏览器做一帧一帧的请求,等到浏览器有空余帧,则执行更新队列中的任务。
React 底层通过 MessageChannel 实现了一个 requsetIdleCallback,可以兼容不同的浏览器系统。底层原理参考宏任务在事件循环中的执行。如果当前环境不支持 MessageChannel,会自动开启 setTimeout 的降级方案。
MessageChannel 作用
MessageChannel 允许在不同的浏览上下文,比如 window.open() 打开的窗口或者 iframe 等之间建立通信管道,并通过两端的端口(port1和port2)发送消息。MessageChannel 底层实现以 DOM Event 的形式发送消息,所以它属于异步的宏任务,会在下一个事件循环的开头执行,并且执行的时机早于 setTimeout。
我们熟悉的 web worker 跟主线程的通信就是基于 MessageChannel 实现的。
const { port1, port2 } = new MessageChannel();
port1.onmessage = function (event) {
console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
};
port2.onmessage = function (event) {
console.log('收到来自port1的消息:', event.data); // 收到来自port1的消息: ping
port2.postMessage('pong');
};
port1.postMessage('ping');
浏览器本身也是基于event loop的,如果浏览器允许0ms,可能会导致一个很慢的js引擎不断被唤醒,从而引起event loop阻塞,对于用户来说就是网站无响应。所以chrome 1.0 beta更改限制为1ms,但是后来发现1ms也会导致CPU spinning,计算机无法进入睡眠模式,经过多次实验后,Chorme团队选定了4ms
2. 任务中断
任务中断就是在 reconciler 和 scheduler 中两个 workLoop 循环执行 break。
- 中断:在 scheduler 中每次执行 workLoop 循环时会根据条件判断是否中断任务执行,即 break
- 保存:reconciler 的 performConcurrentWorkOnRoot 方法,会在执行时,通过逻辑判断,返回不同的值,当返回的值为其自身时,可以视作是一种中断前的状态保存
function performConcurrentWorkOnRoot(){
// 当 fiber 链表的 callbackNode 在执行时,并没有发生改变
// 说明当前任务和之前是相同的任务,即上一次执行的任务还可以继续
// 便将其自身返回,用于 scheduler 中的 continuationCallback
if (root.callbackNode === originalCallbackNode) {
return performConcurrentWorkOnRoot.bind(null, root);
}
... ...
}
3. 任务恢复
任务恢复就是保证没有执行完的任务(中断的任务)不出栈,即当前的 task 没有执行完,还可以在下一次循环时继续执行。
scheduleUpdateOnFiber
在React中无论是初始化还是更新state,内部调用的都是scheduleUpdateOnFiber方法。scheduleUpdateOnFiber可以看做是整个React应用的入口。
1.内部主要做的事情
-
通过当前的更新优先级lane,将当前fiber到Rootfiber的父级链表上的所有优先级更新
-
在非批量更新状态下,直接执行更新任务
-
useState和setState任务会进入ensureRootIsSchedule调度流程
-
当前执行的任务类型为NoContext时,会调用flushSyncCallbackQueue执行任务队列中的任务
可控任务:React事件系统事件、addEventListenter监听事件
非可控任务:延时器(Timer)、微任务队列(Microtask)
- 主要函数
markUpdateLaneFromFiberToRoot:向上调和更新优先级
performSyncWorkOnRoot:直接进入调和阶段更新fiber树状态
ensureRootIsSchedule:进入调度流程
flushSyncCallbackQueue:执行任务队列里面的任务

4. 位运算
计算机存储数据时采用的是二进制方式,位运算就是对二进制位进行运算操作。
常用位运算包括:
- &:都为1,则返回1;
- |:都为0,则返回0;
- ^:只有一个1,才返回1
- ~:反转操作,0返回1,1返回0
- ‘<<:向左移动n比特位
- ‘>>’:向右移动n比特位
位运算在 react 中的应用
-
更新优先级(位掩码):每一次更新时,会把待更新的 fiber 增加一个更新优先级,称为 Lane,Lane 的值越小其优先级越高
-
更新上下文(位掩码):每一次更新时,会通过 ExecutionContext 判断现在的更新上下文
-
更新标识(位掩码):每一次更新时,会把需要更新的 fiber 搭上更新标识 flags,证明 fiber 是什么更新类型
function batchedEventUpdate(){
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 赋值事件上下文
try{
return fn(a); // 执行函数
}finally{
executionContext = prevExecutionContext; // 重置之前的状态
}
}
七、concurrent 模式新增特性
1. 开启 concurrent 模式
import { createRoot } from 'react-dom/client';
const container = document.getElementById("root");
const root = createRoot(container);
root.render(
<Provider store={store}>
<Router />
</Provider>
);
2. flushSync 提高优先级
将回调函数中的 state 更新任务放在一个较高优先级的更新中,flushSync 在同步条件下,会合并之前的任务。
const handleClick = ()=>{
setTimeout(()=>{
setNumber(1);
})
setNumber(2);
ReactDOM.flushSync(()=>{
setNumber(3);
})
setNumber(4);
}
console.log(number); // 3 4 1
3. useTransition 降低优先级
通过 startTransition 把不是特别迫切的任务隔离开来,降低任务优先级。
底层实现类似于 useState + startTransition
import React, { useState, useTransition } from "react";
const List=()=>{
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setCount(count + 1);
});
}
return (
<>
<h1>{count}</h1>
<div onClick={handleClick} style={{color: isPending ? 'red' : 'black'}}>
+1
</div>
</>
);
}
export default List; // 点击按钮过程中会数据更新前会有字体变为红色的效果
startTransition与传统方式对比
-
startTransition:同步、早于 setTimeout、不会减少渲染次数、可以中断、不会造成页面卡顿
-
setTimeout:异步、会减少渲染次数、不可中断、会造成页面卡顿
防抖/节流:其本质依然是 setTimeout 执行,只是通过减少了执行频率来减少渲染次数
4. useDeferredValue 获取延迟状态
使用 useDeferredValue 包裹某个状态值时,React 会将对该状态值的更新操作进行延迟。如果这个状态值在短时间内多次变化,React 只会取最后一次更新的值,从而避免不必要的渲染。
const Case: React.FC = memo(() => {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
const handleChange = (e:any)=>{
setText(e.target.value)
}
return (
<div>
<input value={text} onChange={handleChange} />
<p>Deferred Text: {deferredText}</p>
</div>
);
});
适用场景:优化输入框的响应速度、优化滚动事件、优化动画效果
5. Suspense + React.lazy
Suspense 组件的 fallback 属性,用来代替 Suspense 处于 loading 状态时的渲染内容。
React.lazy 通过 import() 动态加载组件,返回值为 promise 对象。
渲染流程:React.lazy 通过 throw 返回 Promise 对象,Suspense 接收 Promise 对象,通过 Promise.then 获取 resolve 状态中的组件,并渲染该组件。
const App = React.lazy(() => import("./App"));
const List = React.lazy(() => import("./List/List"));
const CaseTest = React.lazy(()=> import("./CaseTest/CaseTest"));
export default () => (
<Suspense>
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/list" element={<List />} />
<Route path="/test" element={<CaseTest/>}/>
</Routes>
</BrowserRouter>
</Suspense>
);
八、 React 事件系统
1. 原生事件系统
3个阶段
W3C 标准约定了一个事件的传播过程要经过以下3个阶段:事件捕获阶段、目标阶段、事件冒泡阶段。

事件委托
把多个子元素的同一类型的监听逻辑合并到父元素上,通过一个监听函数来管理的行为。将事件绑定在父元素上,利用事件冒泡原理,通过 e.target 判断是否为目标元素,从而决定是否触发事件。
2. react 合成事件系统
合成事件
以click为例,当我们点击页面的某个元素时,React会根据原生事件nativeEvent找到触发事件的 DOM 元素和对应的fiber节点。并以该节点为children节点往上查找,找到包括该节点及以上所有的click回调函数,创建dispatchListener并添加到listeners数组中。
React合成事件就是将同类型的事件找出来,基于这个类型的事件,React通过事件的接口和原生事件创建相应的合成事件实例,并重写了preventDefault和stopPropagation方法。
同类型的事件会复用同一个合成事件实例对象,节省了单独为每一个事件创建事件实例对象的开销,这就是事件的合成
<button
onClickCapture={()=>{console.log('合成事件捕获阶段执行')}}
onClick={()=>{console.log('合成事件冒泡阶段执行')}}
>
按钮命名
</button>
button.addEventListener('click',()=>{console.log('原生事件监听')});
-
统一绑定在 document 或外层容器上:React 的事件处理函数不会直接绑定到真实 DOM 节点上,而是把所有事件都绑定到结构的最外层(React17、18绑定到 root 元素,React16绑定到 document),使用一个统一的事件监听器管理
-
事件可控性:React 可以感知事件的触发,并且让事件变得可控,方便外层 App 统一处理事件
-
合成事件:React 事件系统中将原生事件组合,形成合成事件。一个合成事件可能包括多个原生事件,合成事件在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口。虽然合成事件并不是原生 DOM 事件,但它保存了原生 DOM 事件的引用。可以通过 e.nativeEvent 获取对应的原生事件
-
统一管理:React 使用一个统一的事件监听器管理合成事件。事件监听器上维持了一个映射,保存所有组件内部的事件监听和处理函数。当组件挂载或卸载的时候,在这个事件监听器上插入或删除一些对象;当事件发生时,事件监听器在映射里找到真正的事件处理函数并调用。
3. 合成事件传播
合成事件捕获阶段执行 => 原生事件监听 => 合成事件冒泡阶段执行
export const ListContent = ()=>{
function outerClick() {
console.log('--------outer合成事件---冒泡----');
}
function outerClickCapture() {
console.log('--------outer合成事件---捕获----');
}
function innerClick(e:any) {
console.log('--------inner合成事件---冒泡----');
}
function innerClickCapture(e:any) {
console.log('--------inner合成事件---捕获----');
}
useEffect(() => {
/**
* 原生事件的冒泡和捕捉
*/
document.addEventListener('click', (ev) => {
console.log('document原生事件------冒泡');
});
document.addEventListener('click', (ev) => {
console.log('document原生事件------捕获');
},true);
document.body.addEventListener('click', () => {
console.log('body原生事件------冒泡');
});
document.body.addEventListener('click', () => {
console.log('body原生事件------捕获');
}, true);
document.getElementById('root')?.addEventListener('click', () => {
console.log('root原生事件------冒泡');
});
document.getElementById('root')?.addEventListener('click', () => {
console.log('root原生事件------捕获');
}, true);
document.querySelector('.outer')?.addEventListener('click', () => {
console.log('outer原生事件------冒泡');
});
document.querySelector('.outer')?.addEventListener('click', (ev) => {
console.log('outer原生事件------捕获');
}, true)
document.querySelector('.inner')?.addEventListener('click', (ev) => {
console.log('inner原生事件------冒泡');
});
document.querySelector('.inner')?.addEventListener('click', (ev) => {
console.log('inner原生事件------捕获');
}, true);
});
return(
<div>
<div className = 'outer' onClick = {outerClick} onClickCapture={outerClickCapture} >
outer
<div className = 'inner' onClick = {innerClick} onClickCapture={innerClickCapture} >
inner
</div>
</div>
</div>
)
}

4. 阻止冒泡和默认行为
React 合成事件系统可以看做原生 DOM 事件系统的子集,所以在原生事件中阻止冒泡行为,可以同时阻止 React 合成事件的传播,反之则不行。
React 中阻止原生事件传播使用 ev.stopPropagation() 和 return false,阻止合成事件传播使用 stopPropagation(),阻止默认行为使用 ev.preventDefault()。
React Hook 中每个方法的上下文都指向该组件实例,会自动绑定 this 为当前组件,并且 React 会对 this 的引用进行缓存,以此优化 CPU 和内存。但 class 组件和纯函数组件的自动绑定会失效,需要通过 bind、构造器内声明、箭头函数手动处理 this 指向
5. 合成事件系统原理
1. 事件注册
事件注册是自执行的,也就是 React 自身进行调用
// 注册React事件
registerSimpleEvents();
registerEvents$2();
registerEvents$1();
registerEvents$3();
registerEvents();
registerSimpleEvents
注册大部分事件,在 React 中被定义为顶级事件
- 离散事件:如:
click, keyup, change - 用户阻塞事件:如:
dragEnter, mouseMove, scroll - 连续事件:如:
error, progress, load,
优先级排序:0:离散事件 1:用户阻塞事件 2:连续事件
registerEvents$2:注册类似onMouseEnter,onMouseLeave单阶段事件,只注册冒泡阶段事件
registerEvents$1:注册onChange相关事件,注册冒泡和捕获阶段两个事件
registerEvents$3:注册onSelect相关事件,注册冒泡和捕获阶段两个事件
registerEvents:注册onBeforeInput,onCompositionUpdate等相关事件,注册冒泡和捕获阶段两个事件
2. 事件监听
创建 fiberRoot 时会执行 listenToAllSupportedEvents 方法,用来进行事件监听。
listenToAllSupportedEvents(rootContainerElement);
// rootContainerElement 是应用中的`id = root`的DOM元素
该函数主要遍历上面事件注册添加到 allNativeEvents 的事件,按照一定规则,区分冒泡阶段,捕获阶段,区分有无副作用等再进行监听,监听的api还是 addEventListener。
// 监听冒泡阶段事件
// target:id = root
// listener 是一个事件派发器
function addEventBubbleListener(target, eventType, listener) {
target.addEventListener(eventType, listener, false);
return listener;
}
// 监听捕获阶段事件
function addEventCaptureListener(target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}
3. 事件派发
事件一旦在 id = root 的 DOM 元素中委托,其实是一直在触发的,只是没有绑定对应的回调函数。当我们把鼠标移入我们的应用页面中时,这时就在派发事件了,因为页面的 DOM 元素是有监听 mousemove 之类的事件。
根据 nativeEvent.target 找到真正触发事件的 DOM 元素,并根据 DOM 元素找到对应的 fiber 节点,判断 fiber 节点的类型以及是否已渲染来决定是否要派发事件
遍历
listeners执行上面的listene
React 合成事件中并不存在传统意义上的任务调度(即 macrotask /微任务),合成事件是为了模拟浏览器的原生事件,但它并不使用浏览器的事件循环机制。
React 合成事件处理函数是在 React 的渲染流程中被调用的,这是 React 自己的事件注册和事件分发流程,不依赖于浏览器的事件循环机制。因此,React 合成事件中不存在宏任务(macrotask)或微任务(microtask)的概念。
如果你需要在React事件处理函数中执行异步任务,并且希望在事件处理完成后更新组件,你可以使用 setTimeout 或者其他异步API来实现。
九、 React 性能优化
1. shouldComponentUpdate
React 组件会根据 shouldComponentUpdate 的返回值来决定是否执行该方法之后的生命周期,进而决定是否对组件进行 re-render(重渲染)。
默认值为 true,即无条件的重渲染。
shouldComponentUpdate 可以根据接收的新的 props 和 state 决定是否更新组件,由于 shouldComponentUpdate 采用的是浅比较,如果引用类型的内存地址没有改变,但是属性值改变了,会影响其判断。可以结合 Immutable Data 使用。
适用场景:
- 父组件更新引发的子组件无条件更新
- 组件内部的 state 变化引发的组件更新
shouldComponentUpdate(nextProps,nextState){
// text没有改变则不更新
if(nextProps.text === this.props.text){
return false;
}
return true;
}
2. PureComponent
PureComponent 内置了“在 shouldComponentUpdate 中对组件更新前后的 props 和 state 进行浅比较,并根据浅比较的结果决定是否需要继续更新流程”。
export const class APP extends React.PureComponent{
... ...
}
- 基本数据类型:比较两次的值是否相等
- 引用数据类型:比较两个值的引用是否相等
如果数据没变,但是引用变化,则 PureComponent 还是会进行无用的重渲染;如数据变了,但是引用没变,则 PureComponent 不会重渲染,导致页面显示错误;
为了解决这个问题,需要借助于 Immutable.js。
3. Immutable.js
Immutable Data 表示创建后不能再更改的数据,对 Immutable 对象进行修改、添加、删除操作都会返回一个新的 Immutable 对象。Immutable 的实现原理是持久化的数据结构,在使用旧数据创建新数据时,同时保证旧数据可用且不变。该数据类型避免了深拷贝复制带来的性能损耗。
Immutable Data 使用了结构共享,如果对对象中一个节点进行更新,只修改当前节点和受他影响的父节点,其他节点进行共享。
- 常用数据类型有 Map、List、ArraySet
- 常用的判读方法:is
优点:降低数据变化造成的复杂度、节省内存
import {Map,is} from 'immutable';
let a = Map({
select: 'users',
filter: 'name'
});
let b = a.set('select','people');
a === b // false
a.get('filter') === b.get('filter') // true
is(a) === is(b) // true
4. React.memo
memo 是个高阶组件, 结合了 PurComponent 和 shouldComponentUpdate 功能,会对传入的 props 进行浅比较,从父组件直接隔断子组件渲染。
缓存机制
-
父组件重新渲染,没有被 memo 包裹的子组件也会重新渲染
-
被 memo 包裹的组件只有在 props 改变后,才会重新渲染
-
memo 对新旧 props 做浅比较,对于引用类型的数据如果发生了更改,需要返回一个新的地址
-
memo不能避免组件内部state和context更新引发的重新渲染
浅比较
React 底层使用的是 shallowEqual,其比较流程如下:
- 比较新老 props 或者 state 是否相等,相等就不更新组件
- 判断新老 props 或者 state 是否为对象,不是的话直接更新组件
- 通过 Object.keys 将新老 props 或者 state 的属性名 key 变成数组,判断数组长度是否相等,不相等直接更新组件
- 遍历老 props 或者 state,判断与之对应的新 props 或者 state,是否与之相等(引用内存地址相等),全部相等则不更新组件,否则更新组件
import React, { useState } from "react";
const Child = () => <div>{console.log("子组件又渲染")}</div>;
const List = () => {
const [flag, setFlag] = useState(false);
return (
<>
<Child />
<div onClick={() => setFlag(!flag)}>
{flag ? "显示" : "隐藏"}
</div>
</>
);
};
export default List;

点击按钮,父子组件都重新渲染了
import React, { useState } from "react";
const Child = React.memo(() => <div>{console.log("子组件又渲染")}</div>);
const List = () => {
const [flag, setFlag] = useState(false);
return (
<>
<Child />
<div onClick={() => setFlag(!flag)}>
{flag ? "显示" : "隐藏"}
</div>
</>
);
};
export default List;

点击按钮,只有父组件重新渲染
缓存陷阱
memo 对于新旧 props 的比较是浅比较,当一个引用类型的 props 改变时,只要它的地址没有发生改变,那么就算 props 中某一项数据发生了改变,那么被 memo 包裹的组件是不会重新渲染的
当点击按钮,父组件的状态已经更新,但是子组件没有更新
const Child = React.memo((props:any) => (
<div>
{props.list.map((item:any) => (
<div style={{marginLeft: '20px'}}> {item} </div>
))}
</div>
));
const List = () => {
const [list, setList] = useState([1,2,3]);
const handleClick = ()=>{
list.push(4)
setList(list);
console.log(list,'list');
}
return (
<>
<Child list={list}/>
<div onClick={handleClick}>按钮</div>
</>
);
};

解决办法:改变props的引用地址,即返回一个新的数组
const handleClick = ()=>{
setList([...list, 4])
console.log(list,'list');
}

React.memo和forwardRef搭配使用
const Item = forwardRef((props,ref) => {})
const NewItem = React.memo(Item);
export default NewItem;
5. useMemo
useMemo 可以理解为“无副作用的因变量”
const y = useMemo(()=> x+1,[x])
useMemo 会记录上一次的返回值并将其绑定在 fiber 对象,只要组件不销毁,缓存值就一直存在,如果依赖项改变,会重新计算缓存值。
useMemo 接受两个参数 callback 和 deps,useMemo 执行 callback 后,会返回一个结果,并把这个结果缓存起来。当 deps 依赖发生改变的时候,会重新执行 callback 计算并返回新的结果,否则就使用缓存的结果。
useMemo 是在渲染期间完成计算的,所以其返回值可以直接参与渲染。
1)缓存机制
-
依赖数组不为空:组件首次渲染时计算值,依赖项不变会始终返回初始值,依赖项发生改变会重新计算结果并缓存
-
依赖数组为空:组件首次渲染时计算值,后续渲染将重用这个值,而不进行重新计算
-
省略依赖数组:相当于没有使用 useMemo
2)特点
- useMemo 是对计算的结果进行缓存,当缓存结果不变时,会使用缓存结果
- useMemo 搭配 memo 实现子组件重新渲染的性能优化
- 依赖数组中避免包含不稳定的值,如内联函数或对象,可能会导致不必要的重新计算
原因:父组件将引用类型传递给子组件,当子组件用 memo包裹,memo 就会对 props 做浅比较,父组件重新渲染时,会在内存中开辟一个新的地址赋值给引用类型,引用类型的地址发生变化,子组件会重新渲染。所以需要使用 useMemo 对引用数据进行缓存。

3)应用场景
当点击计算后,调用 setNum 后会重新渲染组件,从而导致 computeResult 也跟着重新计算了,浪费性能
const Parent = () => {
const [num, setNum] = useState(0);
const clickHadler = () => {
setNum(num + 1)
}
const computeResult = () => {
// 模拟需要花费时间的大量计算
for(let i = 0; i < 10000; i++) {}
}
return (
<>
{computeResult(),number值: {num}
<Button onClick={() => clickHadler()}>点击计算</Button>
</>
);
};
可以使用 useMemo 进行优化,从而减少不必要的状态更新
// 使用 useMemo 缓存计算的结果
const computeResult = useMemo(() => {
for(let i = 0; i < 10000; i++) {}
}, [])
6. useCallback缓存函数
缓存函数,当函数依赖项发生变化时会重新创建函数并返回新函数地址,否则会直接返回旧的回调函数地址,react hook 中渲染性能优化的钩子函数。
useCallBack 的底层不是在依赖不变的情况下阻止函数创建,而是在依赖不变的情况下不返回新的函数地址而返回旧的函数地址,不论是否使用 useCallBack 都无法阻止组件 render 时函数的重新创建。
1)缓存机制
-
依赖数组不为空:组件首次渲染时创建函数并返回函数地址,依赖项不变会始终返回旧函数地址,依赖项发生改变会重新创建新函数并返回新函数地址
-
依赖数组为空:组件首次渲染时创建函数并返回函数地址,后续渲染将始终用旧的函数地址,而不进行重新创建
-
省略依赖数组:相当于没有使用 useCallback
2)特点
-
useCallback 可以单独使用,但是单独使用的性能优化没有实质的提升,当父组件重新渲染时,子组件同样会渲染
-
useCallback 需要配合 memo 一起使用,当父组件重新渲染时,缓存的函数的地址不会发生改变,memo 浅比较会认为 props 没有改变,因此子组件不会重新渲染

7. PureRender强化
PureRender 强化 shouldComponentUpdate 生命周期
可以在 shouldComponentUpdate 生命周期通过对 nextProps 与 prevProps、nextState 与 prevState 做浅比较,返回 false 阻止 render 方法执行,来减少不必要的更新,从而提升性能
import React,{Component} from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
class App extends Component {
constructor(props){
super(props);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
}
8. 强制更新
类组件的 forceUpdate,函数组件改变内存地址,以及 Context 更新引发消费者更新等,都会造成 React 应用的强制更新。
9. 判断组件是否更新
判断一个组件是否更新可以通过如下流程:
第一步:判断组件内部是否开启优化策略,useMemo、useCallback 等。如果使用 useMemo 或者 useCallback,则会在其依赖项改变时才会重新计算缓存
第二步:判断子组件是否使用 React.memo 包裹,或者继承自 pureComponent。如果 class 继承自 pureComponent,则开启浅比较,返回值相等则不更新;如果使用 React.memo 包裹函数组件,继续判断是否传入第二个参数用来自定义判断流程,如果没有第二个参数,则使用浅比较,否则使用第二个参数返回值决定是否更新;
第三步:如果是类组件判断是否有 shouldComponentUpdate 生命周期。如果有的话判断返回值是否为 true,true 的话更新组件,否则不更新组件;