任务调度中requestIdleCallback的不足
scheduleCallback
实现了按时间切片的任务调度, 浏览器自带的API requestIdleCallback
能达到时间切片的效果,但react最终未采用,主要由于以下原因
- 部分浏览器不支持,如Safar、andriod@40以下的webview等
- 精确度不足,浏览器的渲染和事件行为有可能导致现有任务的执行卡顿,即任务有可能被断断续续的打断
浏览器执行以下代码:
// item元素为蓝色, item11元素为红色
window.onload = () => {
document.body.onclick = () => {
const d = document.createElement('div')
d.className = 'item11'
root.append(d)
}
const root = document.getElementById('root')
for (let i = 0; i < 500 * 100; i++) {
let a = i + 1
requestIdleCallback(() => {
const d = document.createElement('div')
d.className = 'item'
root.append(d)
const arr = []
for (let a = 0; a < 20 * 200; a++) {
const arr2 = []
for (let b = 0; b < 10 * 10; b++) {
arr2.push(b)
}
arr.push(arr2)
}
})
}
初始化页面的时候快速连续点击页面得到下面结果:
可以看到点击生成的元素是离散分布的,而按react的scheduleCallback
实现的点击结果的频率是更为平整的:
两种scheduleCallback的实现方式
离开了原生的requestIdleCallback,还能想到什么方式去实现将控制权转交给浏览器?
时间切片实时记录当前任务的开始时间,切片时间用完则停止任务,通过异步下一次任务来把控制权转交给浏览器。
实现方式MessageChannel
/ setTimeout
/ setImmediate
+ while
+ 递归, 实现requestDDCallback
,即下一批次任务执行的再触发,以下实现未实现任务优先级调度、延时任务调度、手动暂停任务等功能;
const t = []
let getCurrentTime
let isWorking
let startTime
let frameInterval = 5
const hasPerformanceNow =
// $FlowFixMe[method-unbinding]
typeof performance === 'object' && typeof performance.now === 'function';
if (hasPerformanceNow) {
const localPerformance = performance;
getCurrentTime = () => localPerformance.now();
} else {
const localDate = Date;
const initialTime = localDate.now();
getCurrentTime = () => localDate.now() - initialTime;
}
// todo 兼容性判断使用`MessageChannel` / `setTimeout` / `setImmediate`的哪一种
function requestDDCallback(callback) {
const mess = new MessageChannel()
mess.port1.onmessage = callback
mess.port2.postMessage(null)
}
function requestHostCallback() {
if (!isWorking) {
requestDDCallback(startUnitWork)
}
}
function startUnitWork() {
const hasMore = unitWork()
if (hasMore) {
startTime = getCurrentTime()
requestHostCallback()
}
}
// timeout来设置任务的过期时间,react中timeout越大优先级越低
function schedule(callback, timeout = -1, hightLevel = false) {
const startTime_ = getCurrentTime()
const work = {
startTime: startTime_,
callback,
exprationTime: startTime_ + timeout
}
if (!hightLevel) {
t.push(work)
} else {
t.unshift(work)
}
startTime = getCurrentTime()
requestHostCallback()
}
function shouldYield() {
if (getCurrentTime() - startTime < frameInterval) {
return false
} else return true
}
function unitWork() {
let ct = t[0]
isWorking = true
while (ct) {
if (shouldYield() && ct.exprationTime ) {
break
}
ct.callback()
t.shift()
ct = t[0]
}
isWorking = false
let hasMore = t.length !== 0
return hasMore
}
window.schedule = schedule
对比requestIdleCallback
,手动实现的scheduleCallback
也存在不足,由于js线程是单线程执行,scheduleCallback
无法将任务转给其他的异步插入的js任务如setTimeout
、setInterval
等,requestIdleCallback是可以的。
原文链接:https://juejin.cn/post/7332402033281482802 作者:空镜