来看这么一个题目,有一个耗时任务,比如下面这样
<div id="time"></div>
<div>点击按钮后,会同时执行1000个耗时任务</div>
<div><span id="click-btn">开始执行任务</sapn></div>
function delay() {
let now = Date.now()
while(Date.now() - now < 5) {}
}
const clickBtn = document.getElementById('click-btn');
clickBtn.addEventListener('click', async() => {
let now = Date.now()
for (let i = 0; i < 1000; i++) {
await runTask(delay)
}
let time = Date.now() - now
const timeDiv = document.getElementById('time');
timeDiv.textContent = `运算耗时${time}`
})
要求我们写一个runTask函数,满足以下两点
- 要尽快完成任务,同时不能让页面产生卡顿
- 尽量兼容更多浏览器
这个题目是什么意思呢,如果我们直接执行会发生什么
function runTask(task) {
task()
}
当我们事件监听的任务开始执行时,其实是delay函数执行了1000次,导致渲染主线程一直在占用,渲染任务不能及时执行,从而导致页面卡死
那应该怎么办呢,可以使用微任务么?
function runTask(task) {
return new Promise((resolve) => {
// 微任务仍然会卡顿
Promise.resolve().then(() => {
task()
resolve()
})
})
}
肯定是不行的,渲染主线程要把微任务队列清空才会去调用渲染任务的,所以同直接执行一样,仍然会产生阻塞
使用宏任务呢
function runTask(task) {
return new Promise((resolve) => {
setTimeout() => {
task()
resolve()
}, 0)
})
}
宏任务的话不会造成卡顿,但是画面不流畅
因为事件循环实际上是这样的
for(;;) {
取出一个宏任务
执行宏任务
...
if(渲染机制到达) {
渲染
}
}
像我们这里1000个任务是都在任务队列中等着呢,关键就是这个渲染机制到达,不同浏览器处理是不一样的
像谷歌可能就会降低刷新频率来执行更多的任务,而safari则仍然遵从着16.6ms一次,所以在谷歌中看的就会有些卡顿
使用requestAnimationFrame呢
function runTask(task) {
return new Promise((resolve) => {
requestAnimationFrame() => {
task()
resolve()
})
})
}
requestAnimationFrame虽然不会造成卡顿,但是不满足尽快执行的规则
因为每一帧只执行了一个task,这就导致了执行损耗,白白浪费了时间
其实到这里我们应该已经想到了办法,既然这样我们是不是可以手动控制每一帧执行的任务呢
其实伪代码就是
if(当前帧是否有剩余时间) {
task()
callback()
} else {
_runTask(task, callback)
}
最终代码
计算当前帧的剩余时间可以使用requestIdleCallback,由于兼容性不好,使用requestAnimationFrame改写一下
function runTask(task) {
return new Promise((resolve) => {
_runTask(task, resolve)
})
}
function _runTask(task, callback) {
// 但是这个方法有兼容性问题,safari就不认这个方法
// requestIdleCallback((idle) => {
// if (idle.timeRemaining() > 0) {
// task()
// callback()
// } else {
// _runTask(task, callback)
// }
// })
// 所以我们使用requestAnimationFrame来实现
// 得自己实现每一帧执行多少任务
let now = Date.now()
requestAnimationFrame(() => {
if (Date.now() - now < 1000 / 60) {
task()
callback()
} else {
_runTask(task, callback)
}
})
}