什么是优先级
通过更新的心智模型,我们了解到更新
具有优先级
。
那么什么是优先级
?优先级
以什么为依据?如何通过优先级
决定哪个状态应该先被更新?
本节我们会详细讲解。
什么是优先级
在React理念一节我们聊到React
将人机交互研究的结果整合到真实的UI
中。具体到React
运行上这是什么意思呢?
状态更新
由用户交互
产生,用户心里对交互
执行顺序有个预期。React
根据人机交互研究的结果
中用户对交互
的预期顺序为交互
产生的状态更新
赋予不同优先级。
具体如下:
生命周期方法:同步执行。
受控的用户输入:比如输入框内输入文字,同步执行。
交互事件:比如动画,高优先级执行。
其他:比如数据请求,低优先级执行。
如何调度优先级
我们在新的React结构一节讲到,React
通过Scheduler
调度任务。
具体到代码,每当需要调度任务时,React
会调用Scheduler
提供的方法runWithPriority
。
该方法接收一个优先级
常量与一个回调函数
作为参数。回调函数
会以优先级
高低为顺序排列在一个定时器
中并在合适的时间触发。
对于更新来讲,传递的回调函数
一般为状态更新流程概览一节讲到的render阶段的入口函数
。
你可以在==unstable_runWithPriority== 这里看到
runWithPriority
方法的定义。在这里看到Scheduler
对优先级常量的定义。
例子
优先级最终会反映到update.lane
变量上。当前我们只需要知道这个变量能够区分Update
的优先级。
接下来我们通过一个例子结合上一节介绍的Update
相关字段讲解优先级如何决定更新的顺序。
在这个例子中,有两个Update
。我们将“关闭黑夜模式”产生的Update
称为u1
,输入字母“I”产生的Update
称为u2
。
其中u1
先触发并进入render阶段
。其优先级较低,执行时间较长。此时:
fiber.updateQueue = {
baseState: {
blackTheme: true,
text: 'H'
},
firstBaseUpdate: null,
lastBaseUpdate: null
shared: {
pending: u1
},
effects: null
};
在u1
完成render阶段
前用户通过键盘输入字母“I”,产生了u2
。u2
属于受控的用户输入,优先级高于u1
,于是中断u1
产生的render阶段
。
此时:
fiber.updateQueue.shared.pending === u2 ----> u1
^ |
|________|
// 即
u2.next === u1;
u1.next === u2;
其中u2
优先级高于u1
。
接下来进入u2
产生的render阶段
。
在processUpdateQueue
方法中,shared.pending
环状链表会被剪开并拼接在baseUpdate
后面。
需要明确一点,shared.pending
指向最后一个pending
的update
,所以实际执行时update
的顺序为:
u1 -- u2
接下来遍历baseUpdate
,处理优先级合适的Update
(这一次处理的是更高优的u2
)。
由于u2
不是baseUpdate
中的第一个update
,在其之前的u1
由于优先级不够被跳过。
update
之间可能有依赖关系,所以被跳过的update
及其后面所有update
会成为下次更新的baseUpdate
。(即u1 -- u2
)。
最终u2
完成render - commit阶段
。
此时:
fiber.updateQueue = {
baseState: {
blackTheme: true,
text: 'HI'
},
firstBaseUpdate: u1,
lastBaseUpdate: u2
shared: {
pending: null
},
effects: null
};
在commit
阶段结尾会再调度一次更新。在该次更新中会基于baseState
中firstBaseUpdate
保存的u1
,开启一次新的render阶段
。
最终两次Update
都完成后的结果如下:
fiber.updateQueue = {
baseState: {
blackTheme: false,
text: 'HI'
},
firstBaseUpdate: null,
lastBaseUpdate: null
shared: {
pending: null
},
effects: null
};
我们可以看见,u2
对应的更新执行了两次,相应的render阶段
的生命周期勾子componentWillXXX
也会触发两次。这也是为什么这些勾子会被标记为unsafe_
。
如何保证状态正确
现在我们基本掌握了updateQueue
的工作流程。还有两个疑问:
render阶段
可能被中断。如何保证updateQueue
中保存的Update
不丢失?有时候当前
状态
需要依赖前一个状态
。如何在支持跳过低优先级状态
的同时保证状态依赖的连续性?
我们分别讲解下。
如何保证Update
不丢失
在上一节例子中我们讲到,在render阶段
,shared.pending
的环被剪开并连接在updateQueue.lastBaseUpdate
后面。
实际上shared.pending
会被同时连接在workInProgress updateQueue.lastBaseUpdate
与current updateQueue.lastBaseUpdate
后面。
具体代码见这里
当render阶段
被中断后重新开始时,会基于current updateQueue
克隆出workInProgress updateQueue
。由于current updateQueue.lastBaseUpdate
已经保存了上一次的Update
,所以不会丢失。
当commit阶段
完成渲染,由于workInProgress updateQueue.lastBaseUpdate
中保存了上一次的Update
,所以 workInProgress Fiber树
变成current Fiber树
后也不会造成Update
丢失。
如何保证状态依赖的连续性
当某个Update
由于优先级低而被跳过时,保存在baseUpdate
中的不仅是该Update
,还包括链表中该Update
之后的所有Update
。
考虑如下例子:
baseState: ''
shared.pending: A1 --> B2 --> C1 --> D2
其中字母
代表该Update
要在页面插入的字母,数字
代表优先级
,值越低优先级
越高。
第一次render
,优先级
为1。
baseState: ''
baseUpdate: null
render阶段使用的Update: [A1, C1]
memoizedState: 'AC'
其中B2
由于优先级为2,低于当前优先级,所以他及其后面的所有Update
会被保存在baseUpdate
中作为下次更新的Update
(即B2 C1 D2
)。
这么做是为了保持状态
的前后依赖顺序。
第二次render
,优先级
为2。
baseState: 'A'
baseUpdate: B2 --> C1 --> D2
render阶段使用的Update: [B2, C1, D2]
memoizedState: 'ABCD'
注意这里baseState
并不是上一次更新的memoizedState
。这是由于B2
被跳过了。
即当有Update
被跳过时,下次更新的baseState !== 上次更新的memoizedState
。
跳过
B2
的逻辑见这里
通过以上例子我们可以发现,React
保证最终的状态一定和用户触发的交互
一致,但是中间过程状态
可能由于设备不同而不同。
:::details 高优先级任务打断低优先级任务demo
:::
参考资料
深入源码剖析componentWillXXX为什么UNSAFE
React Core Team Andrew向网友讲解Update工作流程的推文