Promise
, setTimeout
, requestAnimationFrame
, requestIdleCallback
这几个概念相信很多人都很熟悉了,最近在看 React Fiber
源码的时候又对它们有了更深一层的认识,在此分享一下。下文将用 rAF
代表 requestAnimationFrame
, rIC
代表 requestIdleCallback
。
事件循环和上面 4 个名词的基本概念在此不再啰嗦了,我们着重看下它们之间的关系。浏览器是一个 UI 系统,所有的操作最终都会以页面的形式展现,而页面的基本单位是帧。一帧中可能包括的任务有下面几种类型。
setTimeout
Promise
requestAnimationFrame
requestIdleCallback
理想情况下,页面会以 60 帧每秒的帧率来运行,但实际上每秒绘制多少帧是由多个因素决定的,下面举一些例子:
rAF
制作动画的时候,浏览器会尽可能快的重绘页面,桌面浏览器可能是 60 帧,移动浏览器可能是 30 帧。从上面的例子可以看出,页面的帧率不是固定的,是会动态变化的。当某一帧的任务占用大量时间的时候,会影响到下一帧的执行。那么谁来调节帧率呢?显然只能依靠浏览器自身。作为开发者的我们是无法准确预知回调什么时候执行的。比如:
function animation() {
console.log('time: ', +new Date());
setTimeout(animate, 1000 / 60);
}
animation();
上面的函数假定了浏览器以帧率 60 来运行,但当帧率达不到的时候,2 帧之间回调可能执行了多次,也可能一次都不执行,简称掉帧。
所以在制作动画的时候,我们不能预设浏览器的帧率,正确的做法是通过 rAF
注册回调, 由浏览器来控制动画调用时机:
function animation() {
console.log('time: ', +new Date());
requestAnimationFrame(animation);
}
animation();
rAF
会保证注册的回调在下次渲染页面之前执行,且只会执行一次。另外,当页面处于不可见状态时,rAF
会自动停止执行,以节省系统资源。
Promise
, setTimeout
, rAF
和 rIC
对应 4 种队列:微任务队列、宏任务队列、animation 队列和 idle 队列。
setTimeout(()=>console.log('setTimeout'), 0);
Promise.resolve().then(()=>console.log('promise'));
requestAnimationFrame(()=>console.log('animation'));
requestIdleCallback(()=>console.log('idle'));
// 执行结果大多数情况下是: promise, animation, setTimeout, idle
// 少数情况是:promise, setTimeout, animation, idle
再来谈谈空闲时间怎么理解。假设在 1 秒内有 3 帧需要渲染:
rAF
占用的时间不多,有大量的空闲时间与rAF
类似,rIC
的执行时机是由浏览器控制的,能更好的保证体验,优化性能。一般优先级高的任务(如 UI 更新)会放在 rAF
队列,优先级低的任务(如日志上传)会放 rIC
。
在一个事件循环内,各个队列有以下特性:
function loop() {
Promise.resolve().then(loop);
}
loop();
React Fiber
就是用这个机制。但最新版的 React Fiber
已经不用 rIC
了,因为调用的频率太低,改用 rAF
了 本文介绍了 4 种队列的执行顺序和每个队列的特性,它们是:宏任务队列、微任务队列、animation 队列和 idle 队列。实际应用时可以根据它们各自的特点分配不同的任务。