任务的可中断执行是react
的concurrence
模式下重要的特性。
what ? 什么是任务的可中断执行
在执行一个长时间任务的时候,将任务分成多个阶段,每个阶段结束之后去检测当前是否需要停止当前任务,如果需要就停止,不需要就继续执行。
react
在每次任务执行结束,以及fiber
构建(fiber构建
本身就是一个任务)的每个fiber
构建之后(workInProgress
每次更新)的时候都会检测是否需要停止当前fiber
构建去执行更高优先级的任务
why ? 为什么需要可中断执行的任务
主要是javascript
的单线程执行机制。如果一个任务执行太长时间,线程一直被使用,如果这时候用户有交互操作,就不能及时得到响应,就有了“页面很卡”的感觉。
react
中的更新过程分为两个阶段:render阶段
和commit阶段
。render阶段
是来构建fiber
树的。如果你的html
结构很复杂,层级很深,那么自顶而下的fiber
树构建需要花费很长时间。这时候用户的操作,比如Input
的输入,按钮的点击等等就被阻塞,而导致用户产生卡顿的感觉。而react
源码中就使用了任务的可中断执行,让优先级高的任务先执行,尽快让用户感知变化。
How ? 怎么让任务可中断执行?
从任务可中断执行的表明意思可以提取出两个重要信息:
- 任务执行过程中可以中断
- 任务中断之后可以继续执行。
任务可中断
如果一个长时间任务的执行抽象成多个阶段,且每个阶段的执行都是类似的工作,那么就可以用while
抽象如下:
while(true) {
// 每个阶段都是执行以下函数
doWorkForCurrentLoop()
}
此时,如果在每个while
循环之前去检测一下,是否能够继续执行下一个阶段的任务,如果不满足条件就退出while
循环,这样就模拟了任务的可中断执行。至于这个检查标准,考虑可中断是为了解决长时间执行导致的阻塞问题,可以设置一个任务可执行的最大时间(react
源码中使用的是5ms
),超过这个时间就退出。此时代码如下:
const MaxTime = 5 // 5ms
function shouldYield() {
return performance.now() - lastRecordingTime /*任务开始时间*/ > MaxTime
}
// 通过 shouldYield 函数来判断是否需要中断
while(!shouldYield()) {
// 每个阶段都是执行一下函数
doWorkForCurrentLoop()
}
此时就实现了任务的可中断执行
任务中断之后继续执行
继续执行是指在之前的状态上继续执行。举个🌰,比如:你需要打印1~100
,第一次任务执行打印了1 ~ 40
的之后中断了执行。 继续执行是指 从打印41
开始你的任务。显然这里是需要维护一个状态的。在上面的代码中加入一个状态量wip
。
const MaxTime = 5 // 5ms
let wip = null // 全局的状态量
function shouldYield(startTime) {
return (performance.now() - startTime) > MaxTime
}
// start为 mayTaskNeedLongTime 开始执行的时间
function mayTaskNeedLongTime(startTime) {
// 通过 shouldYield 函数来判断是否需要中断
while(wip && !shouldYield(startTime)) {
// 每个阶段都是执行一下函数
doWorkForCurrentLoop(wip)
}
}
function doWorkForCurrentLoop(wip) {}
将while
循环封装成一个函数,如上,此时他就是一个任务。接下里需要一个调度器来调度任务队列,也让中断的任务继续执行。
调度器实现
首先确认调度器该有的能力:
- 任务调度:从任务队列中获取任务并执行,这里有一定标准,
react
中以过期时间或者说优先级为标准去做调度 - 执行时机:如果用同步的方式去执行,那么可中断就没有任务意义。这里用异步任务的方式去执行,这样浏览器的渲染就不会被阻塞。
MessageChannel
实现微任务执行
const messageChannel = new MessageChannel()
messageChannel.port1.onmessage = function () {
// 加入微任务队列
}
// 触发 onmessage 执行
messageChannel.port2.postMessage(null)
维护任务队列,来管理任务
const taskQueue = []
// 一个任务是带有callback的对象
// 将上述的 mayTaskNeedLongTime 加入task队列
taskQueue.push({
callback: mayTaskNeedLongTime
})
执行队列里的任务
let lastRecordingTime = -1 // 任务开始时间
function workLoop() {
while(taskQueue.length) {
const top = taskQueue[0]
// 记录开始时间
const startTime = lastRecordingTime = performance.now()
// 执行任务
top.callback(startTime)
// 执行完 弹出任务
top.shift()
}
}
队列任务执行的可中断
这里跟上述的可中断类似,也可以通过 break
while
循环退出任务执行。
let lastStartTimeExecutingTask = -1 // 任务开始时间
function workLoop() {
while(taskQueue.length) {
if (lastStartTimeExecutingTask !== -1 && performance.now() > (lastStartTimeExecutingTask + MaxTime)) {
break
}
const top = taskQueue[0]
// 记录开始时间
const startTime = lastStartTimeExecutingTask = performance.now()
// 执行任务
top.callback(startTime)
// 执行完 弹出任务
top.shift()
}
}
任务可继续执行
在执行任务的时候,如果当前回调函数的返回值是另一个函数,那么这个返回的函数就是保存之前状态的接下来要继续执行的函数。继续改造代码
function mayTaskNeedLongTime(startTime) {
// 通过 shouldYield 函数来判断是否需要中断
while(wip && !shouldYield(startTime)) {
// 每个阶段都是执行一下函数
doWorkForCurrentLoop(wip)
}
// 返回一个函数,中断之后继续执行的函数
return mayTaskNeedLongTime
}
function workLoop() {
while(taskQueue.length) {
if (lastStartTimeExecutingTask !== -1 && performance.now() > (lastStartTimeExecutingTask + MaxTime)) {
break
}
const top = taskQueue[0]
// 记录开始时间
const startTime = lastStartTimeExecutingTask = performance.now()
// 执行任务,同时接受返回值
/******************继续执行的关键*********************/
const continuous = top.callback(startTime)
// 之前的callback已经消费掉,替换为新的callback,可以继续执行
// 这样下一次的while循环就可以 调用 continuous 函数了,实现可继续执行
// 同时wip还保存着之前的状态
/***************************************/
if (typeof continuous === 'function') {
top.callback = continuous
} else {
// 执行完 弹出任务
top.shift()
}
}
// 表明任务执行完成 队列已清空
return taskQueue.length === 0
}
微任务实现任务队列执行
messageChannel.port1.onmessage = function () {
if (scheduledCallback !== null) {
const isFinish = scheduledCallback()
if (isFinish) {
console.log('任务执行结束')
} else {
// 重新让任务进入 微任务队列 执行
lastStartTimeExecutingTask = -1
messageChannel.port2.postMessage(null)
}
}
}
function requestCalllback(callback) {
scheduledCallback = callback
// 在下一个微任务队列里 执行
messageChannel.port2.postMessage(null)
}
requestCalllback(workLoop)
到此,任务调度,可中断,中断之后的继续执行已完成。
任务队列里可以添加其他任务,同时可以在任务对象(现在只有callback)中添加其他属性,比如优先级等等,来扩展能力。
该链接里有上述完整的代码,同时也给任务添加了优先级,实现任务队列执行过程中中断并去执行更高优先级的任务。