Scheduler调度
在React
中,有一个单独的Scheduler
库专门用于处理上面讨论的时间切片。
我们简单看一下Scheduler
关键源码实现:
- 首先,在
packagegs/react-reconciler/src/ReactFiberWorkLoop.new.js
文件中:
// 循环更新 fiber 节点
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
// 更新单个 fiber 节点
performUnitOfWork(workInProgress);
}
}
在更新时,如果是Concurrent
模式,低优先级更新会进入到workLoopConcurrent
函数。该函数的作用就是遍历Fiber
节点,创建Fiber
树并标记哪些Fiber
被更新了。performUnitOfWork
表示的是对每个Fiber
节点的处理操作,每次处理前都会执行shouldYield()
方法,下面看一下shouldYield
。
- 其次,在
packages/scheduler/src/forks/Scheduler.js
文件中:
export const frameYieldMs = 5;
let frameInterval = frameYieldMs;
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
// 判断时间间隔是否小于 5ms
if (timeElapsed < frameInterval) {
return false;
}
...
}
shouldYield()
方法会去判断累计更新的时间是否超过5ms
。
- 最后,在
packages/scheduler/src/forks/Scheduler.js
文件中:
let schedulePerformWorkUntilDeadline;
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);
};
}
如果超过了5ms
,就会通过schedulePerformWorkUntilDeadline
开启一个宏任务进行下一个更新。这里react
做了兼容的处理,实际上是优先使用MessageChannel
而不是setTimeout
,这是因为在浏览器帧中MessageChannel
更优先于setTimeout
执行。
总的来说,Scheduler
库的处理和前面讨论的时间切片类似。事实上,浏览器也正在做同样的Scheduler
库做的事情:通过内置一个api
——scheduler.postTask 来解决用户交互在某些情况下无法即时相应的问题,有兴趣的话可以看看相关内容。
最终,通过这种时间切片的方式,在浏览器下的performance
面板中,会呈现出如下渲染过程:原本一个耗时的更新(如渲染10000
个li
标签),被分割为一个个5ms
的小更新:
到这里,我们已经清楚了如何让一个耗时的更新不去阻塞用户事件和渲染
了。但是这只是有一个更新任务的情况,如果在React
更新一半时,click
事件进来,然后执行click
事件回调,并且触发了新的更新,那么该如何处理共存的两个更新呢?如果click
事件的更新过程中,又有其他的click
事件触发更新呢?这就涉及到多个更新并存的情况,这也是我们接下来需要讨论的点。