setTimeout
setTimeout() 可以设置一个延迟,延迟多少秒将一个 callback 放入到宏任务队列。
虽然设置了延迟时间,但不是到了延迟时间,callback 就会马上执行。
callback 只是被放到了宏任务队列中,当调用栈空闲的时候,才会从宏任务队列中取一个任务执行。
如果调用栈的任务还没处理完,就会延迟了宏任务队列中的任务的执行时间。
setTimeout(callback, 0) 零延迟的 setTimeout
这里的 0 其实是放入到宏任务队列的延迟,而不是放入调用栈的延迟。
因此,不能保证 callback 会马上被执行,依旧需要等待调用栈任务(当前的同步任务) 执行完成。
如果调用栈内的任务执行需要很久,那 callback 也可能等很久才会被执行。
setInterval
setInterval() 是每隔一段时间,就把 callback 加入到任务队列。
它只是把任务加入到了任务队列,任务未必马上就执行。
如果间隔时间小于任务执行时间,例如,间隔时间为 5s,callback 执行时间需要 20s。
那么,每 5s 加入一个任务,假设经过了 20s, 则有四个任务 A, B, C, D。
当 A 从任务队列出队执行时,A 需要执行 20s。
B,C,D 都得等 A 执行完才能执行,于是 B, C, D 都被堆在任务队列中。
如果设想的是每隔 5s,就执行 callback,那么上面这种情况显然是不符合的。
A,B,C,D 并不是间隔 5s 就执行,而只是间隔 5s 被放入到队列中。
当 callback 执行时间可能大于间隔时间时,用 setInterval 就不合适了。
对于长时间的同步任务
var fakeTimeIntensiveOperation = function() { for (var i = 0; i< Math.pow(10, 10); i++) {} let insideTimeTakingFunction = new Date().toLocaleTimeString(); console.log('insideTimeTakingFunction', insideTimeTakingFunction); }; var timer = setInterval(function(){ fakeTimeIntensiveOperation(); let insideSetInterval = new Date().toLocaleTimeString(); console.log('insideSetInterval', insideSetInterval); }, 1000);
时间很长的同步任务会阻塞 setInerval 的执行,并没有按照每一秒的频率去执行。
对于长时间的异步任务
let counter = 0; var fakeTimeIntensiveOperation = function() { setTimeout(() => { counter--; }, 60000); }; var timer = setInterval(function() { counter++; fakeTimeIntensiveOperation(); let insideSetInterval = new Date().toLocaleTimeString(); console.log('insideSetInterval', insideSetInterval); console.log('counter', counter); }, 200);
对于执行时间长的异步任务,例如接口请求,接口的响应可能需要很长时间,这就会造成大量请求的堆积。
想保持 callback 每隔一段时间执行,可以使用嵌套的 setTimeout 实现。
嵌套的 setTimeout
(function loop() { setTimeout(function() { // your logic here loop(); }, delay); })()
嵌套的 setTimeout 可以确保 callback 间隔一段时间执行,避免了 setInterval 的弊端。
除此之外,嵌套的 setTimeout 还可以根据 callback 的执行情况,调整延迟时间。
例如轮询请求接口,如果发现服务器已经超过负载,没法处理更多的请求了,可以延长轮询时间,降低服务器的负载。
let delay = 5000; let timerId = setTimeout(function request() { // ...发送请求... if (request failed due to server overload) { // 下一次执行的间隔是当前的 2 倍 delay *= 2; } timerId = setTimeout(request, delay); }, delay);
requestAnimationFrame
Window.requestAnimationFrame() 主要是用来做动画的。
要想形成动画,就需要至少每秒 24 帧,这样才能让静态的东西看起来在动。
但是 24 帧其实还不够,往往要达到 60 帧左右,动画才会看起来顺滑。
实现动画可以用 setInterval 或者嵌套的 setTimeout,设置 60 fps (1000 / 60) 的间隔,不断地进行改变。
但是这两个 api 都有一些弊端,可能会被其他同步任务阻塞,导致不能及时地更新动画,而出现丢帧。
而且这两个 api 也不会考虑当前的 tab 是否显示去暂停或开始,导致性能消耗可能比较大。
而 requestAnimationFrame 则是为了解决这些问题出现的,它有几个优点:
- 不会被当前的同步任务阻塞,不会出现卡帧问题,它总是在屏幕下一次重绘之前去调用 callback,相对于 setInterval 更稳定。
- 不用指定间隔,而是根据显示器的刷新率调整 callback 的调用频率。
如果浏览器 tab 没有被激活 / 选中,或者元素不可见,那么 requestAnimationFrame 就会暂停,减少了性能消耗。
当需要去实现动画时,应该优先去考虑 requestAnimationFrame 。
requestIdleCallback
window.requestIdleCallback() 用于在浏览器空闲的时候,去调用 callback。
在每一帧渲染的最后,如果完成帧的渲染,还有空余时间,就可以利用这段空余的时间去执行 callback,避免影响渲染,动画等。
如果浏览器一直处于满载状态,requestIdleCallback 注册的 callback 有可能一直都不会执行。
requestIdleCallback(callback, options) 中的第二个参数是 options。
可以指定一个 timeout,表示如果超过了 timeout 还没有找到空闲时间去执行,则强制执行 callback,不再等待空闲。
建议使用的时候都指定 timeout。
由于是利用帧的最后一点空闲时间去执行 callback,callback 做的事情应该比较简单,耗时不要太长。
例如统计数据上传、数据预加载等。
另外由于帧已经渲染完成,尽量不要在 requestIdleCallback 再去改变 DOM, 避免造成新的重绘,影响下一帧的渲染。