React
中异步可中断更新是Concurrent
模式最重要的一个特性。那如何保证任务中断之后的状态时正确的呢?
首先看这样一个组件:
function Counter() {
const [count, dispatch] = useState('start: ')
console.log('render text', count)
return (
<div>
<button
onClick={() => {
// 以不同的优先级调用
// 3: NormalPriority
// 1: ImmediatePriority
unsafe_setCurrentPriorityLevel(3)
dispatch((state) => state + 'A')
unsafe_setCurrentPriorityLevel(1)
dispatch((state) => state + 'B')
unsafe_setCurrentPriorityLevel(3)
dispatch((state) => state + 'C')
unsafe_setCurrentPriorityLevel(1)
dispatch((state) => state + 'D')
}}
>
123
</button>
<div>{count}</div>
</div>
)
}
这里 unsafe_setCurrentPriorityLevel
是可以修改调度优先级的函数,即可以手动修改 react-schduler
包中的变量 currentPriorityLevel
的。如果对调度工作原理不太熟,可以理解为 通过这个函数,dispatch
这个动作就有对应的优先级,1
表示高优先级,而3
优先级较1
低,表示普通优先级。
当执行完上述函数之后,Counter
这个函数组件所对应的fiber
的memoizedState
中会存在一个如下图的循环链表。(源码中,会先在dispatchAction
函数中形成单向循环列表挂在queue.pending
,然后在updateReducer
拷贝到baseQueue
中,这里以状态变更为准,不去考虑这些。)
上图中每个方块表示一个Update
,蓝色表示高优先级
,透明表示低优先级
。baseQueue
指向最新的一条更新。
这里需要说明一点的是:在以不同优先级调用dispatch
时,会将优先级记录(源码中pendingLanes
),然后处理时先取最高优先级(源码中是用lanes
去处理优先级,这里简化记为Priority_Flag
,伪代码中会用到),所以这里会优先执行蓝色方块的Update
。
通过baseQueue.next
可以拿到链表的第一个Update
,下面通过伪代码模拟针对每一个Update
做的处理
newState = 'start: '
cloneUpdate = null
// FirstUpdate = A
// 开始: Update 指向 A
do {
// 如果`Update`优先级比
if (Update.Priority < Priority_Flag) {
// 克隆时保留优先级
newUpdate = cloneUpdate(Update)
// cloneUpdate也是单向循环链表,这里将 newUpdate 拼接上去
concatNewUpdate(newUpdate)
}
// 如果当前 Update 的优先级 大于等于 Hight_Priority,就执行当前 Update
newState = reduce(newState, action)
// 当存在优先级较低的 Update时,
// 也需要 clone 满足优先级的Update
// 但是这里有一个很重要的点:需要将 优先级 改成最高优先级(红色)
// 目的是 使用最高优先级之后,后续无论什么优先级,这个Update都会被使用
if (cloneUpdate !== null) {
newUpdate = cloneUpdate(Update)
}
// 拼接
concatNewUpdate(newUpdate)
Update = Update.next // 指向下一个 Update
} while(Update !== null || Update !== FirstUpdate)
通过上述代码之后,高优先级的Update
即上述蓝色方块的Update
会被使用:
Update-B
通过newState = reduce(newState, action)
得到更新之后的结果为 newState = start: B
Update-D
通过newState = reduce(newState, action)
得到更新之后的结果为 newState = start: BD
之后将cloneQueue
再拷贝给BaseQueue
然后通过commit
阶段,第一次打印的结果为 render text start: BD
,因为含有未处理完的任务,所以会再次进入调度处理。
因为高优先级已被处理,这时的Priority_Flag
为普通优先级。
如果再去执行一次上述的伪代码,那么所有的Update
都会被执行,且cloneUpdate
一直为null
:
Update-A
: 普通优先级,可以执行,结果为 start: A
Update-B
: 最高优先级,一定会执行执行,结果为 start: AB
Update-C
: 普通优先级,可以执行,结果为 start: ABC
Update-D
: 最高优先级,一定会执行执行,结果为 start: ABCD
所以这一次的结果为 start: ABCD
。
这里主要是利用链表维护了所有待更新的Update
,在每一次调度中执行符合对应优先级的更新,同时保留所有未更新以及更新的Update
的在链表中的先后关系,下一次重新从头计算状态,这样每次计算的状态都是符合要求的。