前言
react 16.8
版本引入了fiber
架构,在fiber架构的支持下,实现了并发渲染。
- 在16.8版本以前,在
state
改变后,经过一系列的流程,最终会进入diff,在diff的时候,会采用DFS进行遍历虚拟dom树,这个遍历过程是一口气
完成的,当我们的应用很复杂,元素比较多的时候,diff的过程往往会花费很长的时间,主线程此时正在进行diif比较,如果diff事件超过16.6ms,会造成浏览器掉帧,如果时间很长,会造成页面卡顿。 - 在react18版本,默认开启了并发模式,开发者可以使用
useTransition
hook手动开并发模式,在并发模式下,diff
过程是可中断的,如果diff
时间超过5ms
,那么React会中断此次diff
,会交出主线程的使用权,从而让浏览器去渲染,当有空闲的时间的时候,会利用时间循环机制恢复diif
,从而提高大型应用的用户体验。并发模式下任务的打断与恢复机制是scheduler
提供的,我们今天将探讨下,如何利用React scheduler打断一个耗时任务,例如从0累加到2000万。
scheduler
从0累加到2000万
ini
const work = () => {
for(let currentIndex = 0; currentIndex < totalCount; currentIndex++) {
sum += currentIndex
}
}
- 火焰图
- 我们看上面的火焰图,这个任务执行了近100ms,左右,超过了浏览器的一帧
16.6ms
,当一个任务执行时间超过50ms
时,浏览器会任务这个任务时长任务,超过50ms的部分,火焰图会标记为红色。 - 既然同步遍历会造成浏览器掉帧,那么我们有什么办法做到不让浏览器掉帧呢?答案就是将这2000万个累加操作分成很小的
task
,然后让task
作为一个工作单元去执行,当执行到一定的数量的时候,如果运行时间超过了我们规定的时间,那么我们中断这个任务,在中断的时候判断下还有没有任务要执行,如果有任务要执行,那么我们将这个任务执行函数放入事件循环中,等下一次事件循环的时候,取出来执行这个任务。
- 如何让for循环中断呢? 我们只需要在遍历的时候判断当前的索引和数据总数以及抽象出一个方法,判断应不应该终止(比如:我规定让task累计运行5ms,当5ms用完的时候,就应该终止)。
javascript
const getCurrentTime = () => Date.now();
// 调度应该被中断吗
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
// 如果当前时间减去开始时间小于 5ms, 那么继续调度
if (timeElapsed < 5) {
return false;
}
return true;
}
const work = () => {
for(let currentIndex = 0; currentIndex < totalCount && !shouldYieldToHost(); currentIndex++) {
sum += currentIndex
}
}
- 如何判断有没有任务需要恢复呢? 我们将
work
函数放在workloop
函数中去执行,当work
函数终止的时候,调用栈回到workloop
函数时,判断currentIndex < totalCount
,如果小于,那么我们reutrn true
,告诉调用workloop
的函数还有任务需要执行,需要将任务执行函数放到异步任务中,等下一次事件循环的时候取出来执行。
javascript
const flushWork = () => {
return workLoop()
}
const workLoop = () => {
while(true) {
try {
work()
} catch (error) {
}finally {
if(currentIndex < totalCount) {
return true
} else {
return false
}
}
}
}
- 任务恢复机制,利用事件循环原理
javascript
const performWorkUntilDeadline = () => {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true; // 有剩余时间
let hasMoreWork = true;
try {
// 这里执行的函数就是 flushWork,flushWork 如果返回一个 true 那么表示还有任务
// 这里是 workLoop 循环里 return 的, 如果 return true, 那么表示还有剩余的任务,只是时间用完了,被中断了
hasMoreWork = flushWork();
} finally {
//如果还有剩余任务,调用schedulePerformWorkUntilDeadline将performWorkUntilDeadline 放入到异步任务里,等下一次事件循环被调用。
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
}
}
let schedulePerformWorkUntilDeadline;
// react 中调度的优先级 setImmediate > MessageChannel > setTimeout
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
完整实现代码
javascript
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null;
let startTime; // 记录开始时间
let sum = 0
const currentIndex = 0; // 当前遍历的索引
const totalCount = 20000000
const getCurrentTime = () => Date.now();
// 调度应该被中断吗
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
// 如果当前时间减去开始时间小于 5ms, 那么继续调度
if (timeElapsed < 5) {
return false;
}
return true;
}
const performWorkUntilDeadline = () => {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true; // 有剩余时间
let hasMoreWork = true;
try {
// 这里执行的函数就是 flushWork,flushWork 如果返回一个 true 那么表示还有任务
// 这里的 是 workLoop 循环里 return 的, 如果 return true, 那么表示还有剩余的任务,只是时间用完了,被中断了
hasMoreWork = flushWork();
} finally {
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
}
}
};
let schedulePerformWorkUntilDeadline;
// react 中调度的优先级 setImmediate > MessageChannel > setTimeout
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
const flushWork = () => {
return workLoop()
}
const workLoop = () => {
while(true) {
try {
work()
} catch (error) {
}finally {
if(currentIndex < totalCount) {
return true
} else {
return false
}
}
}
}
const work = () => {
for(let currentIndex = 0; currentIndex < totalCount && !shouldYieldToHost(); currentIndex++) {
sum += currentIndex
}
}
performWorkUntilDeadline()
- 火焰图
- 我们可以看到,上面执行近100ms的任务被分成了很多很多小任务,其中每个小任务执行的时间是5ms左右。这样我们就完成了利用react scheduler实现任务的打断与恢复机制。