注:本文使用的
react
版本为v18.2.0
,并发更新下的状态计算所在的文件路径:/packages/react-reconciler/src/ReactFiberHooks.js
|/packages/react-reconciler/src/ReactFiberClassUpdateQueue.js
,分别对应函数组件和类组件,下面以函数组件进行讲解
并发模式是 react 16
中引入的新特性(最早叫做异步模式,后来 react 团队为了避免与其他异步渲染方法的混淆而更名),它通过使用新的 fiber
架构来实现,随之带来的功能有时间分片和优先级调度等,值得一提的是虽然这些特性在 react 16
就已经存在,但是一直都是以不稳定的特性存在(react 18
才稳定下来),所以在 react 18
之前包括 react 18
都没有默认开启(react 18
中可以通过 useTransition
开启),而今天要讲的状态计算就与优先级有关
首先来看一个例子 --- 🔗
css
/* App.css */
.container {
overflow-y: auto;
border: 1px solid #000000;
width: 1200px;
height: 400px;
word-break: break-all;
}
jsx
// App.jsx
import { useState, useTransition } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const [_, startTransition] = useTransition();
return (
<div className="container">
<button
onClick={() => {
startTransition(() => {
setCount((preCount) => {
console.log(preCount);
return preCount + 1;
});
});
}}
>
低优先级
</button>
<button
onClick={() => {
setCount((preCount) => {
console.log(preCount);
return preCount + 2;
});
}}
>
高优先级
</button>
<br />
{new Array(100000).fill(0).map((_, index) => {
return <span key={index}>{count}</span>;
})}
</div>
);
}
export default App;
点击低优先级按钮后再点击高优先级按钮(动作稍微快点)
通过上面的图片我们可以很清楚的看到,数字是从 0 -> 2 -> 3
而不是 0 -> 1 -> 3
,原因就是被 startTransition
包裹的更新属于低优先级更新,会被后面的高优先级更新打断,通过这个例子我们能很好地体会到 react
快速响应用户的理念
还有一些可以值得思索的地方,在第一次高优先级更新任务执行完之后,第二次恢复执行低优先级更新任务高优先级 update
还需不需要参与计算,还是直接基于高优先级 update
计算出的状态继续更新呢,还是看上面的代码,setCount
里面都加了 console.log
,控制台里面输出的是 0 0 0 1
,第一个 0
是 startTransition
中的 setCount
产生的 update
,也就是低优先级 update
参与计算输出的,第二个 0
是高优先级 update
参与计算输出的 ,后面的 0 1
证明了第二次恢复执行低优先级更新任务两个 update
都需要参与计算,保证了最终的结果与预期的一致。
这里还有一个有趣的点,试着点击低优先级按钮后迅速 再点击高优先级(动作再快一些),你会发现输出变了,咋回事呢,读者可以停下来想想(提示一下:回忆一下 useTransition
的实现)
js
function startTransition(setPending, callback, options) {
// ...
// 此处的 setPending 产生高优先级 update
setPending(true);
// 计算优先级时,会根据 ReactCurrentBatchConfig.transition 是否 null 来判断是否应该返回 transitionLane
ReactCurrentBatchConfig.transition = {};
// ...
try {
// 此处的 setPending 产生低优先级 update
setPending(false);
// startTransition 传入的回调
callback();
} finally {
// ...
}
}
从 useTransition
的实现我们可以发现里面存在两个 setPending
,关键就在第一个 setPending
,第一个 setPending
属于高优先级更新,它会先开始执行一轮更新任务,在这个更新任务期间我们迅速点击了高优先级按钮,又产生了另一个高优先级 update
,也就是 setCount
对应的高优先级 update
,待到执行完第一个 setPending
对应的更新任务后,会通过 scheduler
调度会复用上一次的高优先级任务,此时的低优先级 setCount
就不会再开启更新了(即低优先级 update
不会参与计算,不会输出任何内容),所以第一个 0
的输出是高优先级 setCount
对应的 update
对象参与计算产生的,后续输出的 0 1
,就跟之前的逻辑一样,恢复执行低优先级更新产生的
总结一下,0 0 1
和 0 0 0 1
的区别就是在哪个阶段点击高优先级按钮,如果处于 setPending
对应高优先级任务执行的过程,输出 0 0 1
,如果处于 setCount
对应低优先级任务执行的过程,输出 0 0 0 1
OK 言归正传,这里 react
解决了两个问题
- 保证优先级的情况下又能保证
update
依赖的连续性 - 多次更新中保持
update
不丢失
那么第一个问题, react
是怎么做的在既保证优先级的情况下又能保证 update
依赖的连续性呢,让我们来分析一下:
首先是每次更新都有一个优先级,只有优先级足够的 update
对象才会参与本次更新的状态计算,
然后来看一下 useState hook
对应的数据结构
ts
type Hook = {
// 保存着 hook 的状态值,比如 useState 保存的就是
// const [state, setState] = useState(); 中的 state
memoizedState: any;
// 保存着第一个被跳过的 update 对象的前一个 update 对象计算出来的值
baseState: any;
// 保存着第一个被跳过的 update 对象以及后面的 update 对象的一条环状链表
baseQueue: Update<any, any> | null;
queue: any;
next: Hook | null;
};
baseState baseQueue
代表的意义可能有点难理解,下面举个例子说明
plain
假如现在我们有一条 update 链表如下 queue.pending:
字母代表 state,数字代表优先级(越小优先级越高)
A1 -> B2 -> C1 -> D2
目前有两种优先级的 update 要处理
第一次更新优先级:1
A1、C1 参与计算
B2、D2 被跳过
本次更新结束时
memoizedState: C
baseState: A
baseQueue: B2 -> C0 -> D2 (C 的优先级变成 0 的原因是 0 代表 NoLane, 属于任何 lanes 集合,这样下次更新时肯定能参与到状态计算中)
第二次更新优先级:2
B2 -> C0 -> D2 参与计算
本次更新结束时
memoizedState: D
baseState: D
baseQueue: null
用一个流程图表示可能更容易理解
updateReducer
:update
计算的主要逻辑
js
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const current = currentHook;
let baseQueue = current.baseQueue;
// 待处理的 update 对象链表
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
// 存在待处理的 update 对象
if (baseQueue !== null) {
// 合并 pendingQueue 和 baseQueue
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// 合并完之后存在要处理 update 对象
// baseQueue 中第一个 update 对象
const first = baseQueue.next;
let newState = current.baseState;
// 要赋给 baseState 的值
let newBaseState = null;
// 如果存在跳过的 update 对象,那么就需要保存跳过的 update 对象以及后面的 update 对象
// 从名字也可以看出来,新的 baseQueue 的第一个节点
let newBaseQueueFirst = null;
// 新的 baseQueue 的最后一个节点
let newBaseQueueLast = null;
let update = first;
do {
// 这里简化了一些逻辑
const updateLane = update.lane;
// 判断 update 优先级是否在本次的 lanes 集合中
const shouldSkipUpdate = !isSubsetOfLanes(renderLanes, updateLane);
if (shouldSkipUpdate) {
// update 优先级不够
// 克隆一下 update
const clone = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
// 形成环状 baseQueue 的逻辑
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
// newBaseQueueLast === null 代表还没有 update 对象被跳过
// 所以这里的 newState 就是第一个被跳过的 update 对象的前一个 update 对象计算出来的值
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else {
// update 优先级足够
if (newBaseQueueLast !== null) {
const clone = {
// NoLane 的原因上面解释过了
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: null,
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// 处理 update
const action = update.action;
if (update.hasEagerState) {
// eagerState 是 react 中性能优化策略,这里不深究
newState = update.eagerState;
} else {
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
// 不存在被跳过的 update 对象
// memoizedState 等于 baseState
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
}
if (!is(newState, hook.memoizedState)) {
// 新旧 state 不一样,标记更新是否进入 bailout 逻辑,
// bailout 逻辑也是 react 中的性能优化策略,这里不深究
markWorkInProgressReceivedUpdate();
}
// 更新结束,赋值
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
queue.lanes = NoLanes;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
通过上面我们已经知道了 react
怎么做到既保证优先级的情况下又能保证 update
对象的依赖的连续性,那么还剩最后一个问题,react
怎么做保证多次更新中低优先级的 update
不丢失
答案是由于 fiber
中的双缓存机制,Hook
也对应存在 currentHook
和 workInProgressHook
,上面 updateReducer
有这么两段逻辑
js
// current === currentHook
let baseQueue = current.baseQueue;
// ...
// hook === workInProgressHook
hook.baseQueue = newBaseQueueLast;
当低优先级更新第一次被打断,由于没有走完一次更新流程, current fiber
树和 workInProgress fiber
树没有互换,低优先级 update
肯定不会丢失
当恢复到低优先级更新时,因为 react
每走完一次更新流程,都会把 current fiber
树和 workInProgress fiber
树互换,这样就表示在多次更新中 currentHook.baseQueue
可以看做是上一次更新的 workInProgressHook.baseQueue
,低优先级的 update
会被跳过且保存到 workInProgressHook.baseQueue
中,此时也能从 currentHook.baseQueue
恢复完整 update
链表
参考资料