We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
大家好,我卡颂。
React内部最难理解的地方就是**「调度算法」**,不仅抽象、复杂,还重构了一次。
React
可以说,只有React团队自己才能完全理解这套算法。
既然这样,那本文尝试从React团队成员的视角出发,来聊聊**「调度算法」**。
React在 v16 之前面对的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于:更新流程是**「同步、不可中断的」**。
为了解决这个问题,React提出Fiber架构,意在**「将更新流程变为异步、可中断的」**。
Fiber
最终实现的交互流程如下:
onClick
useEffect
优先级
render
在render过程中,如果又触发交互流程,步骤 2 又选出一个更高优先级,则之前的render中断,以新的优先级重新开始render。
更高优先级
本文要聊的就是步骤 2 中的**「调度算法」**。
**「调度算法」**需要解决的最基本的问题是:如何从众多更新中选择其中一个更新的优先级作为本次render的优先级?
最早的算法叫做expirationTime 算法。
expirationTime 算法
具体来说,更新的优先级与**「触发交互的当前时间」及「优先级对应的延迟时间」**相关:
// MAX_SIGNED_31_BIT_INT 为最大 31 bit Interger update.expirationTime = MAX_SIGNED_31_BIT_INT - (currentTime + updatePriority);
例如,高优先级更新 u1、低优先级更新 u2 的updatePriority分别为 0、200,则
updatePriority
`MAX_SIGNED_31_BIT_INT - (currentTime + 0) > MAX_SIGNED_31_BIT_INT - (currentTime + 200)
// 即 u1.expirationTime > u2.expirationTime;
`
代表u1优先级更高。
u1
expirationTime 算法的原理简单易懂:每次都选出所有更新中**「优先级最高的」**。
除此之外,还有个问题需要解决:如何表示**「批次」**?
**「批次」**是什么?考虑如下例子:
`// 定义状态 num const [num, updateNum] = useState(0);
// ... 某些修改 num 的地方 // 修改的方式 1 updateNum(3); // 修改的方式 2 updateNum(num => num + 1);
两种**「修改状态的方式」**都会创建更新,区别在于:
更新
num
由于第二种方式的存在,更新之间可能有连续性。
所以**「调度算法」计算出一个优先级后,组件render时实际参与计算「当前状态的值」**的是:
「计算出的优先级对应更新」 + 「与该优先级相关的其他优先级对应更新」
这些相互关联,有连续性的更新被称为一个**「批次」**(batch)。
batch
expirationTime 算法计算**「批次」**的方式也简单粗暴:优先级大于某个值(priorityOfBatch)的更新都会划为同一批次。
priorityOfBatch
const isUpdateIncludedInBatch = priorityOfUpdate >= priorityOfBatch;
expirationTime 算法保证了render异步可中断、且永远是最高优先级的更新先被处理。
这一时期该特性被称为Async Mode。
Async Mode
Async Mode可以解决以下问题:
可中断
这些问题统称为CPU 密集型问题。
CPU 密集型问题
在前端,还有一类问题也会影响体验,那就是**「请求数据造成的等待」**。这类问题被称为IO 密集型问题。
IO 密集型问题
为了解决IO 密集型问题的,React提出了Suspense。考虑如下代码:
Suspense
`const App = () => { const [count, setCount] = useState(0);
useEffect(() => { const t = setInterval(() => { setCount(count => count + 1); }, 1000); return () => clearInterval(t); }, []);
return ( <> <Suspense fallback={
其中:
count
count => count + 1
Sub
fallback
假设请求三秒后返回,理想情况下,请求发起前后UI会依次显示为:
UI
`// Sub 内请求发起前
<div class=“sub”>I am sub, count is 0
// Sub 内请求发起第 1 秒
// Sub 内请求发起第 2 秒
// Sub 内请求发起第 3 秒
// Sub 内请求成功后
<div class=“sub”>I am sub, request success, count is 4
从用户的视角观察,有两个任务在并发执行:
div
Suspense带来了**「多任务并发执行」**的直观感受。
因此,Async Mode(异步模式)也更名为Concurrent Mode(并发模式)。
Concurrent Mode
那么Suspense 对应更新的优先级是高还是低呢?
Suspense 对应更新
当请求成功后,合理的逻辑应该是**「尽快展示成功后的 UI」**。所以Suspense 对应更新应该是高优先级更新。那么,在示例中共有两类更新:
高优先级更新
u0
CPU
u2
u3
在expirationTime 算法下:
// u0 优先级远大于 u1、u2、u3... u0.expirationTime >> u1.expirationTime > u2.expirationTime > …
u0优先级最高,则u1及之后的更新都需要等待u0执行完毕后再进行。
而u0需要等待**「请求完毕」**才能执行。所以,请求发起前后UI会依次显示为:
从用户的视角观察,第二个div被卡住了 3 秒后突然变为 4。
所以,只考虑CPU 密集型场景的情况下,**「高优更新先执行」**的算法并无问题。
CPU 密集型场景
但考虑IO 密集型场景的情况下,高优 IO 更新会阻塞低优 CPU 更新,这显然是不对的。
IO 密集型场景
高优 IO 更新
低优 CPU 更新
所以expirationTime 算法并不能很好支持并发更新。
expirationTime 算法在线 Demo[1]
expirationTime 算法最大的问题在于:expirationTime字段耦合了**「优先级」与「批次」**这两个概念,限制了模型的表达能力。
expirationTime
这导致高优 IO 更新不会与低优 CPU 更新划为同一**「批次」**。那么低优 CPU 更新就必须等待高优 IO 更新处理完后再处理。
如果不同更新能根据实际情况灵活划分**「批次」**,就不会产生这个bug。
bug
重构迫在眉睫,并且重构的目标很明确:将**「优先级」与「批次」**拆分到两个字段中。
新的调度算法被称为Lane,他是如何定义**「优先级」与「批次」**呢?
Lane
对于优先级,一个lane就是一个32bit Interger,最高位为符号位,所以最多可以有 31 个位参与运算。
lane
32bit Interger
不同优先级对应不同lane,越低的位代表越高的优先级,比如:
// 对应 SyncLane,为最高优先级 0b0000000000000000000000000000001 // 对应 InputContinuousLane 0b0000000000000000000000000000100 // 对应 DefaultLane 0b0000000000000000000000000010000 // 对应 IdleLane 0b0100000000000000000000000000000 // 对应 OffscreenLane,为最低优先级 0b1000000000000000000000000000000
「批次」则由lanes定义,一个lanes同样也是一个32bit Interger,代表「一到多个 lane 的集合」。
lanes
可以用位运算很轻松的将多个lane划入同一个批次:
位运算
批次
`// 要使用的批次 let lanesForBatch = 0;
const laneA = 0b0000000000000000000000001000000; const laneB = 0b0000000000000000000000000000001;
// 将 laneA 纳入批次中 lanesForBatch |= laneA; // 将 laneB 纳入批次中 lanesForBatch |= laneB;
上文提到的Suspense的bug是由于expirationTime 算法不能灵活划定批次导致的。
lanes就完全没有这种顾虑,任何想划定为同一**「批次」**的优先级(lane)都能用位运算轻松搞定。
Lane 算法在线 Demo[2]
**「调度算法」**要解决两个问题:
expirationTime 算法中使用的expirationTime字段耦合了这两个概念,导致不够灵活。
Lane 算法的出现解决了以上问题。
Lane 算法
[1]
expirationTime 算法在线 Demo: https://codesandbox.io/s/usetransition-stop-reacting-passed-props-updates-forked-5e7lh
[2]
Lane 算法在线 Demo: https://codesandbox.io/s/usetransition-stop-reacting-passed-props-updates-zoqm2?file=/src/index.js https://mp.weixin.qq.com/s/tkEYtRTrZovA4uVrfnNUDQ
The text was updated successfully, but these errors were encountered:
No branches or pull requests
大家好,我卡颂。
React
内部最难理解的地方就是**「调度算法」**,不仅抽象、复杂,还重构了一次。可以说,只有
React
团队自己才能完全理解这套算法。既然这样,那本文尝试从
React
团队成员的视角出发,来聊聊**「调度算法」**。什么是调度算法
React
在 v16 之前面对的主要性能问题是:当组件树很庞大时,更新状态可能造成页面卡顿,根本原因在于:更新流程是**「同步、不可中断的」**。为了解决这个问题,
React
提出Fiber
架构,意在**「将更新流程变为异步、可中断的」**。最终实现的交互流程如下:
onClick
回调中的更新优先级最高,useEffect
回调中触发的更新优先级一般)优先级
作为本次render
的优先级优先级
对组件树进行render
在
render
过程中,如果又触发交互流程,步骤 2 又选出一个更高优先级
,则之前的render
中断,以新的优先级
重新开始render
。本文要聊的就是步骤 2 中的**「调度算法」**。
expirationTime 调度算法
**「调度算法」**需要解决的最基本的问题是:如何从众多更新中选择其中一个更新的
优先级
作为本次render
的优先级?最早的算法叫做
expirationTime 算法
。具体来说,更新的优先级与**「触发交互的当前时间」及「优先级对应的延迟时间」**相关:
// MAX_SIGNED_31_BIT_INT 为最大 31 bit Interger update.expirationTime = MAX_SIGNED_31_BIT_INT - (currentTime + updatePriority);
例如,高优先级更新 u1、低优先级更新 u2 的
updatePriority
分别为 0、200,则`MAX_SIGNED_31_BIT_INT - (currentTime + 0) > MAX_SIGNED_31_BIT_INT - (currentTime + 200)
// 即
u1.expirationTime > u2.expirationTime;
`
代表
u1
优先级更高。expirationTime 算法
的原理简单易懂:每次都选出所有更新中**「优先级最高的」**。如何表示 “批次”
除此之外,还有个问题需要解决:如何表示**「批次」**?
**「批次」**是什么?考虑如下例子:
`// 定义状态 num
const [num, updateNum] = useState(0);
// ... 某些修改 num 的地方
// 修改的方式 1
updateNum(3);
// 修改的方式 2
updateNum(num => num + 1);
`
两种**「修改状态的方式」**都会创建
更新
,区别在于:num
修改为 3由于第二种方式的存在,
更新
之间可能有连续性。所以**「调度算法」计算出一个
优先级
后,组件render
时实际参与计算「当前状态的值」**的是:「计算出的优先级对应更新」 + 「与该优先级相关的其他优先级对应更新」
这些相互关联,有连续性的
更新
被称为一个**「批次」**(batch
)。expirationTime 算法
计算**「批次」**的方式也简单粗暴:优先级大于某个值(priorityOfBatch
)的更新
都会划为同一批次。const isUpdateIncludedInBatch = priorityOfUpdate >= priorityOfBatch;
expirationTime 算法
保证了render
异步可中断、且永远是最高优先级的更新
先被处理。这一时期该特性被称为
Async Mode
。IO 密集型场景
Async Mode
可以解决以下问题:render
变为可中断
)更新
的优先级
不同)这些问题统称为
CPU 密集型问题
。在前端,还有一类问题也会影响体验,那就是**「请求数据造成的等待」**。这类问题被称为
IO 密集型问题
。为了解决
IO 密集型问题
的,React
提出了Suspense
。考虑如下代码:`const App = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const t = setInterval(() => {
setCount(count => count + 1);
}, 1000);
return () => clearInterval(t);
}, []);
return (
<>
<Suspense fallback={
<Sub count={count} />
</>
);
};
`
其中:
count
更新为count => count + 1
Sub
中会发起异步请求,请求返回前,包裹Sub
的Suspense
会渲染fallback
假设请求三秒后返回,理想情况下,请求发起前后
UI
会依次显示为:`// Sub 内请求发起前
<div class=“sub”>I am sub, count is 0
// Sub 内请求发起第 1 秒
// Sub 内请求发起第 2 秒
// Sub 内请求发起第 3 秒
// Sub 内请求成功后
<div class=“sub”>I am sub, request success, count is 4
`
从用户的视角观察,有两个任务在并发执行:
Sub
的任务(观察第一个div
的变化)count
的任务(观察第二个div
的变化)Suspense
带来了**「多任务并发执行」**的直观感受。因此,
Async Mode
(异步模式)也更名为Concurrent Mode
(并发模式)。一个无法解决的 bug
那么
Suspense 对应更新
的优先级是高还是低呢?当请求成功后,合理的逻辑应该是**「尽快展示成功后的 UI」**。所以
Suspense 对应更新
应该是高优先级更新
。那么,在示例中共有两类更新:Suspense
对应的高优 IO 更新,简称u0
CPU
更新,简称u1
、u2
、u3
等在
expirationTime 算法
下:// u0 优先级远大于 u1、u2、u3... u0.expirationTime >> u1.expirationTime > u2.expirationTime > …
u0
优先级最高,则u1
及之后的更新
都需要等待u0
执行完毕后再进行。而
u0
需要等待**「请求完毕」**才能执行。所以,请求发起前后UI
会依次显示为:`// Sub 内请求发起前
<div class=“sub”>I am sub, count is 0
// Sub 内请求发起第 1 秒
// Sub 内请求发起第 2 秒
// Sub 内请求发起第 3 秒
// Sub 内请求成功后
<div class=“sub”>I am sub, request success, count is 4
`
从用户的视角观察,第二个
div
被卡住了 3 秒后突然变为 4。所以,只考虑
CPU 密集型场景
的情况下,**「高优更新先执行」**的算法并无问题。但考虑
IO 密集型场景
的情况下,高优 IO 更新
会阻塞低优 CPU 更新
,这显然是不对的。所以
expirationTime 算法
并不能很好支持并发更新。出现 bug 的原因
expirationTime 算法
最大的问题在于:expirationTime
字段耦合了**「优先级」与「批次」**这两个概念,限制了模型的表达能力。这导致
高优 IO 更新
不会与低优 CPU 更新
划为同一**「批次」**。那么低优 CPU 更新
就必须等待高优 IO 更新
处理完后再处理。如果不同更新能根据实际情况灵活划分**「批次」**,就不会产生这个
bug
。重构迫在眉睫,并且重构的目标很明确:将**「优先级」与「批次」**拆分到两个字段中。
Lane 调度算法
新的调度算法被称为
Lane
,他是如何定义**「优先级」与「批次」**呢?对于
优先级
,一个lane
就是一个32bit Interger
,最高位为符号位,所以最多可以有 31 个位参与运算。不同优先级对应不同
lane
,越低的位代表越高的优先级,比如:// 对应 SyncLane,为最高优先级 0b0000000000000000000000000000001 // 对应 InputContinuousLane 0b0000000000000000000000000000100 // 对应 DefaultLane 0b0000000000000000000000000010000 // 对应 IdleLane 0b0100000000000000000000000000000 // 对应 OffscreenLane,为最低优先级 0b1000000000000000000000000000000
「批次」则由
lanes
定义,一个lanes
同样也是一个32bit Interger
,代表「一到多个 lane 的集合」。可以用
位运算
很轻松的将多个lane
划入同一个批次
:`// 要使用的批次
let lanesForBatch = 0;
const laneA = 0b0000000000000000000000001000000;
const laneB = 0b0000000000000000000000000000001;
// 将 laneA 纳入批次中
lanesForBatch |= laneA;
// 将 laneB 纳入批次中
lanesForBatch |= laneB;
`
上文提到的
Suspense
的bug
是由于expirationTime 算法
不能灵活划定批次
导致的。lanes
就完全没有这种顾虑,任何想划定为同一**「批次」**的优先级
(lane)都能用位运算
轻松搞定。Lane 算法在线 Demo[2]
总结
**「调度算法」**要解决两个问题:
expirationTime 算法
中使用的expirationTime
字段耦合了这两个概念,导致不够灵活。Lane 算法
的出现解决了以上问题。参考资料
[1]
expirationTime 算法在线 Demo: https://codesandbox.io/s/usetransition-stop-reacting-passed-props-updates-forked-5e7lh
[2]
Lane 算法在线 Demo: https://codesandbox.io/s/usetransition-stop-reacting-passed-props-updates-zoqm2?file=/src/index.js
https://mp.weixin.qq.com/s/tkEYtRTrZovA4uVrfnNUDQ
The text was updated successfully, but these errors were encountered: