我们先来思考一下这个问题,在浏览器的主线程中,JavaScript 执行、样式计算、布局绘制等操作共享同一个运行环境。当遇到耗时任务时,动画卡顿便如影随形。
让我们直接步入正题,首先我们在页面上画一个小球,让他运动起来
import "./App.css";
import { useState } from "react";
const Runtask = () => {
const [left, setLeft] = useState(0);
const animateBall = () => {
let index = 0;
const moveBall = () => {
if (index < 500) {
setLeft((prev) => prev + 1);
index++;
requestAnimationFrame(moveBall); // 每帧执行一次
}
};
requestAnimationFrame(moveBall); // 启动动画
};
return (
<div className="container">
<div
className="moving-ball"
style={{ left: `${left}px` }} // 改用 left 属性
></div>
<button onClick={animateBall}>启动JS动画</button>
</div>
);
};
export default Runtask;
看看效果
接下来我们添加一些耗时任务
import "./App.css";
import { useState } from "react";
const Runtask = () => {
//...
// 耗时任务
const heavyTask = () => {
const start = Date.now();
while (Date.now() - start < 3) {} // 直接阻塞3秒
};
const task = new Array(1000).fill(heavyTask);
const runTask = (task) => {
// ?
};
const onClick = async () => {
console.log("开始耗时任务");
const start = Date.now();
for(let i = 0 ; i < task.length;i++){
runTask(task[i])
}
console.log("任务完成", Date.now() - start);
};
return (
<div className="container">
<div
className="moving-ball"
style={{ left: `${left}px` }} // 改用 left 属性
></div>
<button onClick={onClick}>执行阻塞任务</button>
<button onClick={animateBall}>启动JS动画</button>
</div>
);
};
export default Runtask;
那么问题就来了,runTask这个函数如何编写才能让小球不卡顿呢?
首先,我们先看看直接去调用任务,会发生什么
const runTask = (task) => {
task();
};
可以看到,当开始执行任务之后,小球的运动被阻塞,这个也是意料之中的事情,因为JS单线程,当长时间执行JS代码的时候,主线程一直被占用,所以就不能进行渲染任务。
那我们想一下,既然同步任务不行,我们就试试异步,这时候就迎来了两个问题:微任务、宏任务。
如果我们对时间循环掌握的很好的话,也就可以直接排除微任务了,因为每次事件循环的时候,会将微任务队列清空,再进行下次循环,所以我们就放弃了微任务。
我们再来看看宏任务,
setTimeout(task, 0);
发现还是卡顿了,唉?因为setTimeout计时任务的执行时机,没有官方的标准,完全取决于浏览器的实现。为了证明这个问题,让其他小伙伴帮忙运行:
可以看到,小球还是在运行,只不过是掉帧。
这样肯定还是不能达到我们的效果,我们仔细想一下,既然不能阻塞页面渲染,又还得执行时间。 不阻塞页面渲染,就说明我们要在16ms将主线程交换给渲染线程。 所以我们只能在16ms中抽空去完成任务。
这时候聪明的彦祖们立马就想到了一个API:requestIdleCallback;
来看看这个api的概念:
window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
这不正是我们要找的东西吗??
来实现看看:
const runTask = (task) => {
return new Promise(resove => {
_run(task,resove)
})
};
const _run = (task, callback) => {
requestIdleCallback((dealine) => {
// 判断当前帧是否还有剩余时间
if (dealine.timeRemaining() > 0) {
task();
callback();
} else {
_run(task, callback);
}
});
};
可以发现,当执行任务的时候,丝毫不影响页面的渲染。
看到这里,其实你已经入门了React的任务调度。