计时器
计时器在前端有很多应用场景,比如电商业务中秒杀和抢购活动的倒计时。在探讨计时器之前先来回顾下它们的基本概念:
基本定义与用法
1、定义
setTimeout()
用于指定在一定时间(单位毫秒)后执行某些代码setInterval()
用于指定每隔一段时间(单位毫秒)执行某些代码
2、参数
第一个参数 function
,必填,回调函数。或者是一段字符串代码,但是这种方式不建议使用,就和使用eval()
一样,有安全风险;而且还有作用域问题(字符串会在全局作用域内被解释执行)
// --run--
const fn1 = () => {
console.log("执行fn1");
};
(function() {
const fn2 = () => {
console.log(222222)
};
setTimeout("fn1()", 1000)
})();
// 输入:执行fn1
// --run--
const fn1 = () => {
console.log("执行fn1");
};
(function() {
const fn2 = () => {
console.log(222222)
};
setTimeout("fn2()", 1000)
})();
// 没有输入,全局没有fn2
第二个参数 delay
,可选,单位是 ms
,而不是要执行代码的确切时间。JavaScript
是单线程的,所以每次只能执行一段代码。为了调度不同代码的执行,JavaScript
维护了一个任务队列。其中的任务会按照添加到队列的先后顺序执行。setTimeout()
的第二个参数只是告诉 JavaScript
引擎在指定的毫秒数过后把任务添加到这个队列。如果队列是空的,则会立即执行该代码。如果队列不是空的,则代码必须等待前面的任务执行完才能执行。
第三个参数 param1,param2,param3...
,可选,是传递给回调函数的参数
setTimeout(function (a, b) {
console.log(a, b)
}, 2000, '我是', '定时器')
有一道循环定时器打印题,我们一起来看看
// --run--
for (var index = 0; index < 5; index++) {
setTimeout(() => console.log(index), 1000);
}
// 因为var定义的index变量没有块级作用域的概念,所以每秒打印值都是5
将var
改成let
,使每次定时器回调函数读取自身父级作用域的index
// --run--
for (let index = 0; index < 5; index++) {
setTimeout(() => console.log(index), 1000);
}
传递第三个参数给回调函数可以解决作用域问题
// --run--
for (var index = 0; index < 5; index++) {
setTimeout((idx) => console.log(idx), 1000, index);
}
3、返回值
返回一个 ID
(数字),可以将这个 ID
传递给clearTimeout()
或clearInterval()
来取消执行。setTimeout()
和setInterval()
共用一个编号池,技术上,clearTimeout()
和clearInterval()
可以互换使用,但是为了避免混淆,一般不这么做
setTimeout
setTimeout
与 setInterval
实现原理一致,setTimeout(fn,0)
会将「事件」放入task queue
的尾部,在下一次loop
中,当「同步任务」与task queue
中现有事件都执行完之后再执行。
setInterval
setInterval
如果使用「固定步长」(间隔时间为定值),指的是向队列添加新任务之前等待的时间。比如,调用 setInterval()
的时间为 01:00:00
,间隔时间为 3000
毫秒。这意味着 01:00:03
时,浏览器会把「任务」添加到「执行队列」。浏览器不关心这个「任务」什么时候执行或者执行要花多长时间。因此,到了 01:00:06
,它会再向「队列」中添加一个「任务」。由此可看出,执行时间短、非阻塞的回调函数比较适合 setInterval()
。
时间精度
setTimeout
和setInterval
都存在「时间精度问题」,至少在4ms
以上(4ms
一次 event loop
,也即是最少4ms
才检查一次setTimeout
的时间是否达到),根据浏览器、设备是否插电源等有所不同,最多能达到近16ms
。为了解决这个问题,加快响应速度,产生了「setImmediate API
与 setImmediate.js
项目」与「requestAnimationFrame
」,前者解决「触发之后,立即调用回调函数,希望延迟尽可能短」的情况,后者可以实现「流畅的JS
动画」
// --run--
let last = 0;
let iterations = 10;
function timeout() {
// 记录调用时间
logline(Date.now());
// 如果还没结束,计划下次调用
if (iterations-- > 0) {
setTimeout(timeout, 0);
}
}
function run() {
// 初始化迭代次数和开始时间戳
iterations = 10;
last = Date.now();
// 开启计时器
setTimeout(timeout, 0);
}
function logline(now) {
// 输出上一个时间戳、新的时间戳及差值
console.log('之前:%d,现在:%d,实际延时:%d', last, now, now - last);
last = now;
}
run();
// --run--
let last = 0;
let iterations = 10;
let itv = null;
function timeout() {
// 记录调用时间
logline(Date.now());
// 如果还没结束,计划下次调用
if (iterations-- > 0) {
clearInterval(itv);
}
}
function run() {
// 初始化迭代次数和开始时间戳
iterations = 10;
last = Date.now();
// 开启计时器
itv = setInterval(timeout, 0);
}
function logline(now) {
// 输出上一个时间戳、新的时间戳及差值
console.log('之前:%d,现在:%d,实际延时:%d', last, now, now - last);
last = now;
}
run();
事件积压
如果主线程(或执行栈)中的任务与task queue
中的其它任务再加上setInterval
中回调函数的总执行时间超过了「固定步长」(200ms
),那么setInterval
的回调函数就会「延后执行」,长时间运行就会产生大量「积压」在内存中待执行的函数,如果主线程终于空闲下来,那么就会立刻执行「积压」的大量函数,中间不会有任何停顿。例子如下:(补充:Date.now IE9
以上支持,相对new Date()
来说减少创建一次对象的时间和内存)
// 假设主线程代码执行时长300ms,每个定时回调执行时长300ms,固定步长200ms
// --run--
const itv = setInterval(() => {
const startTime = Date.now();
while(Date.now() - startTime < 300) {}
}, 200);
mainThreadRun(); // 300ms时长
当主线程或者定时器回调函数执行时长越长,「事件积压」就越严重。为了避免长时间运行产生大量「积压」在内存中待执行的函数,产生性能损耗,现在浏览器会保证「当任务队列中没有定时器的任何其它代码实例时,才将新的定时器添加到任务队列」。
事件积压解决方案
如果有些浏览器没有做此优化,一定要使用setInterval
的话,避免事件积压的解决办法有(摘自『javascript
高级程序设计』):
1、间隔时间使用百分比: 开始值 + (目标值 - 开始值) * (Date.now()
- 开始时间)/ 时间区间;
假设有这样一个动画功能需求:把一个div
的宽度从100px
变化到200px
。写出来的代码可能是这样的:
<div id="test1" style="width: 100px; height: 100px; background: blue; color: white;"></div>
function animate1(element, endValue, duration) {
var startTime = new Date(),
startValue = parseInt(element.style.width),
step = 1;
var timerId = setInterval(function() {
var nextValue = parseInt(element.style.width) + step;
element.style.width = nextValue + 'px';
if (nextValue >= endValue) {
clearInterval(timerId);
// 显示动画耗时
element.innerHTML = new Date - startTime;
}
}, duration / (endValue - startValue) * step);
}
animate1(document.getElementById('test1'), 200, 1000);
原理是每隔一定时间(10ms)增加1px
,一直到200px
为止。然而,动画结束后显示的耗时却不止1000ms
,有1011ms
。究其原因,是因为setInterval
并不能严格保证执行间隔。
有没有更好的做法呢?下面先来看一道小学数学题:
A
楼和B
楼相距100米,一个人匀速从A
楼走到B
楼,走了5分钟到达目的地,问第3分钟时他距离A楼多远?
匀速运动中计算某个时刻路程的计算公式为:路程 * 当前时间 / 时间
。所以答案应为 100 * 3 / 5 = 60
。
这道题带来的启发是,某个时刻的路程是可以通过特定公式计算出来的。同理,动画过程中某个时刻的值也可以通过公式计算出来,而不是累加得出:
<div id="test2" style="width: 100px; height: 100px; background: red; color: white;"></div>
function animate2(element, endValue, duration) {
var startTime = new Date(),
startValue = parseInt(element.style.width);
var timerId = setInterval(function() {
var percentage = (new Date - startTime) / duration;
var stepValue = startValue + (endValue - startValue) * percentage;
element.style.width = stepValue + 'px';
if (percentage >= 1) {
clearInterval(timerId);
element.innerHTML = new Date - startTime;
}
}, 16.6);
}
animate2(document.getElementById('test2'), 200, 1000);
这样改良之后,可以看到动画执行耗时最多只会有几毫秒的误差。但是问题还没完全解决,在浏览器开发工具中检查test2
元素可以发现,test2
的最终宽度可能不止200px
。仔细检查animate2
函数的代码可以发现:
percentage
的值可能大于1,可以通过Math.min
限制最大值解决。- 即使保证了
percentage
的值不大于1,只要endValue
或startValue
为小数,(endValue - startValue) * percentage
的值也可能产生误差,因为JavaScript
小数运算的精度不够。其实我们要保证的只是最终值的准确性,所以在percentage
为1的时候,直接使用endValue
即可。
于是,animate2
函数的代码修改为:
function animate2(element, endValue, duration) {
var startTime = new Date(),
startValue = parseInt(element.style.width);
var timerId = setInterval(function() {
// 保证百分率不大于1
var percentage = Math.min(1, (new Date - startTime) / duration);
var stepValue;
if (percentage >= 1) {
// 保证最终值的准确性
stepValue = endValue;
} else {
stepValue = startValue + (endValue - startValue) * percentage;
}
element.style.width = stepValue + 'px';
if (percentage >= 1) {
clearInterval(timerId);
element.innerHTML = new Date - startTime;
}
}, 16.6);
}
2、如果你的代码逻辑执行时间可能比定时器时间间隔要长,建议你使用递归调用了 setTimeout()
的具名函数。例如,使用 setInterval()
以 5 秒的间隔轮询服务器,可能因网络延迟、服务器无响应以及许多其他的问题而导致请求无法在分配的时间内完成。因此,你可能会发现排队的 XHR
请求没有按顺序返回。
在这些场景下,应首选递归调用 setTimeout()
的模式:
(function loop(){
setTimeout(function() {
// Your logic here
loop();
}, delay);
})();
在上面的代码片段中,声明了一个具名函数 loop()
,并被立即执行。loop()
在完成代码逻辑的执行后,会在内部递归调用 setTimeout()
。虽然该模式不保证以固定的时间间隔执行,但它保证了上一次定时任务在递归前已经完成。
setTimeout执行动画
举一个例子来思考下,愤怒的小鸟游戏中,小鸟飞过屏幕时,用户应该在每次屏幕刷新时体验到小鸟以相同的速度前进。假设显示器刷新频率60Hz
(16又2/3
毫秒渲染一次),屏幕将在以下时间(以毫秒为单位)更新:0、16又2/3
、33又1/3
、50
、66又2/3
、83又1/3
、100
等。再假设定时器固定步长15ms
,并(有些乐观地)每帧处理javascript
和渲染只需要0ms
,那么「setTimeout
中设定的时间间隔」+「回调函数执行时间」+「在显示器上绘制/改变动画的下一帧的时间」等于15ms
,每10
『(16 2/3) / ((16 2/3)- 15)=10
』帧会多出一帧来,结果就是在第10
帧的时候,有两个回调动画函数连续执行了,于是动画不再平滑了…(详见这篇啰嗦的文章),更不要说还要考虑setTimeout
的「时间精度」问题(4ms
一次 event loop
,也即是最少4ms
才检查一次setTimeout
的时间是否达到)。
小鸟在屏幕上的X
位置与rAF
处理程序运行时所经过的时间成正比,因为它正在插入鸟的位置,并且rAF
处理将在时间0、15、30、45、60
等处运行。因此,我们可以确定每帧小鸟的视觉X
位置:
第 0 帧,时间 0ms,位置:0,与上一帧的增量:
第 1 帧,时间 16又2/3 毫秒,位置:15,与最后一帧的增量:15
第 2 帧,时间 33又1/3 毫秒,位置:30,与最后一帧的增量:15
第 3 帧,时间 50又0/3 毫秒,位置:45,与最后一帧的增量:15
第 4 帧,时间 66又2/3 毫秒,位置:60,与最后一帧的增量:15
第 5 帧,时间 83又1/3 毫秒,位置:75,与最后一帧的增量:15
第 6 帧,时间 100又0/0 毫秒,位置:90,与最后一帧的增量:15
第 7 帧,时间 116又2/3 毫秒,位置:105,与最后一帧的增量:15
第 8 帧,时间 133又1/3 毫秒,位置:120,与最后一帧的增量:15
第 9 帧,时间 150又0/3 毫秒,位置:150,与最后一帧的增量:30
第 10 帧,时间 166又2/3 毫秒,位置:165,与最后一帧的增量:15
第 11 帧,时间 183又1/3 毫秒,位置:180,与最后一帧的增量:15
第 12 帧,时间 200又0/0 毫秒,位置:195,与最后一帧的增量:15
requestAnimationFrame
requestAnimationFrame
会把每一帧中的所有DOM
操作集中起来,在「一次重绘或回流中就完成」,并且「重绘或回流的时间间隔紧紧跟随浏览器的刷新频率」,一般来说,这个频率为每秒60
帧。
在隐藏或不可见的元素中,requestAnimationFrame
将不会进行重绘或回流,这当然就意味着更少的的cpu
,gpu
和内存使用量。
// --run--
var i = 0, _load = +new Date(), loop = 1000/60;
function f(){
var _now = +new Date();
console.log(i++, (_now-_load)/loop);
_load = _now;
requestAnimationFrame(f);
}
与setTimeout
相比,requestAnimationFrame
不是自己指定回调函数运行的时间,而是跟着浏览器内建的刷新频率来执行回调,这当然就能达到浏览器所能实现动画的最佳效果了。
但另外一方面,requestAnimationFrame
的预期执行时间要比setTimeout
要长,因为setTimeout
的最小执行时间是由「浏览器的时间精度」决定的,但raf会跟随浏览器DOM
的刷新频率来执行,理论为16又2/3ms。但是,在setTimeout
中如果进行了DOM
操作(尤其是产生了重绘)通常不会立即执行,而是等待浏览器内建刷新时才执行。因此对于「动画」来说的话,raf
要远远比setTimeout
适合得多。
rAF
与setTimeout
性能比较:(据某些人说,早期的raf
性能堪忧,尤其是在手机上,反而不如setTimeout
)MacBook Pro Chrome 112.0.5615.137
(正式版本) (arm64
):
setTimeout
用时:30947msrAF
用时:16624ms
并且细心观察,可以发现rAF
的动画效果更加丝滑
setTimeout
性能测试:
// --run--
var raf, i= 1, body = document.querySelector('body');
body.innerHTML = '<div id="sq" style="position:fixed;width:30px;height:30px;top:50px;left:50px;background:red;"></div>';
var sq = document.querySelector("#sq");
var pause = 10;//回调函数执行时间
var _load = +new Date();
var t = 1000/60;
function run1(){
i++;
sq.style.left = sq.offsetLeft + 1 + 'px';
var start = Date.now();
while(Date.now() - start < pause) {}
if(i == 1000){
console.log(Date.now() - _load);
}
raf = setTimeout(run1, t);
}
function stop(){
clearTimeout(raf);
}
run1();
rAF
性能测试:
// --run--
var raf, i= 1, body = document.querySelector('body');
body.innerHTML = '<div id="sq" style="position:fixed;width:30px;height:30px;top:50px;left:50px;background:red;"></div>';
var sq = document.querySelector("#sq");
var pause = 10;//回调函数执行时间
var _load = +new Date();
function run(){
i++;
sq.style.left = sq.offsetLeft + 1 + 'px';
var start = Date.now();
while(Date.now() - start < pause) {}
if(i == 1000){
console.log(Date.now() - _load);
}
raf = requestAnimationFrame(run);
}
function stop(){
cancelAnimationFrame(raf);
}
run();
由于requestAnimationFrame
的特性之一:会把每一帧中的所有DOM
操作集中起来,在「一次重绘或回流中就完成」,因此有github
项目fastdom
。
后台最小超时延迟
为了优化后台标签的加载损耗(以及降低耗电量),浏览器会在「非活动标签」中强制执行一个「最小的超时延迟」。如果一个页面正在使用网络音频 API AudioContext
播放声音,也可以不执行该延迟。
这方面的具体情况与浏览器有关:
Firefox
桌面版和Chrome
针对不活动标签都有一个 「1 秒的最小超时值」。- 安卓版
Firefox
浏览器对不活动的标签有一个至少 15 分钟的超时,并可能完全卸载它们。 - 如果标签中包含
AudioContext
,Firefox
不会对非活动标签进行节流。
// --run--
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
console.log("tab切入后台~")
} else {
console.log("tab切入前台~")
}
})
let last = Date.now();
const itv = setInterval(() => {
const now = Date.now();
console.log('diff:', now - last);
last = now;
}, 500);
倒计时
通常电商业务都会有倒计时功能的秒杀和抢购活动,实现倒计时功能一般会从服务端获取剩余时间来计算,每走动一秒就刷新倒计时显示。
使用 setInterval 实现计时
// --run--
const startTime = Date.now();
let count = 0;
const timer = setInterval(() => {
count++;
console.log("误差:", Date.now() - (startTime + count * 1000) + "ms");
if (count === 10) {
clearInterval(timer);
}
}, 1000)
new Date().getTime() - (startTime + count * 1000)
理想情况下应该是 0ms,然而事实并不是这样,而是存在着误差:
使用 setTimeout 实现计时
// --run--
const startTime = Date.now();
let count = 0;
let timer = setTimeout(func, 1000);
function func() {
count++;
console.log("误差:", Date.now() - (startTime + count * 1000) + "ms");
if (count < 10) {
clearTimeout(timer);
timer = setTimeout(func, 1000);
} else {
clearTimeout(timer);
}
}
setTimeout
也同样存在着误差,而且时间越来越大(setTimeout
需要在同步代码执行完成后才重新开始计时):
为什么会存在误差?
这里涉及到 JS
的代码执行顺序问题, JS
属于单线程,代码执行的时候首先是执行主线程的任务,也就是同步的代码,如果遇到异步的代码块,并不会立即执行,而是丢进任务队列中,任务队列是先进先出,待主线程的代码执行完毕以后,才会依次的执行任务队列中的函数。所以,计时器函数实际执行时间一定大于指定的时间间隔。
因此,对于 setInterval
来说,每次将函数丢进任务队列中,而每次函数的实际执行时间又都是大于指定的时间间隔的,一旦执行的次数多了,误差就会越来越大。
如何得到一个比较准确的计时器?
1、使用while
简单粗暴,我们可以直接用while
语句阻塞主线程,不断计算当前时间和下一次时间的差值。一旦大于等于0,则立即执行。
function intervalTimer(time) {
let counter = 1;
const startTime = Date.now();
function main() {
const nowTime = Date.now();
const nextTime = startTime + counter * time;
if (nowTime - nextTime >= 0) {
console.log('deviation', nowTime - nextTime);
counter += 1;
}
}
while (true) {
main();
}
}
intervalTimer(1000);
// deviation 0
// deviation 0
// deviation 0
// deviation 0
我们可以看到差值稳定在0,但是这个方法阻塞了JS执行线程,导致JS执行线程无法停下来从队列中取出任务。这会导致页面冻结,无法响应任何操作。这是破坏性的,所以不可取。
2、使用requestAnimationFrame
浏览器提供了requestAnimationFrame API
,它告诉浏览器你要执行一个动画,要求浏览器在下次重绘前调用指定的回调函数来更新动画。该回调函数将在浏览器下一次重绘之前执行。每秒执行的次数将根据屏幕的刷新率来确定。60Hz的刷新率意味着每秒会有60次,也就是16.6ms左右。
function intervalTimer(time) {
let counter = 1;
const startTime = Date.now();
function main() {
const nowTime = Date.now();
const nextTime = startTime + counter * time;
if (nowTime - nextTime >= 0) {
console.log('deviation', nowTime - nextTime);
counter += 1;
}
window.requestAnimationFrame(main);
}
main();
}
intervalTimer(1000);
// deviation 5
// deviation 7
// deviation 9
// deviation 12
我们可以发现,根据浏览器帧率执行计时,很容易造成时间不准确,因为帧率不会一直稳定在16.6ms。
3、使用setTimeout
+ 系统时间偏移量
该方案的原理是利用当前系统的准确时间,在每次之后进行补偿校正,setTimeout
保证后续的定时时间为补偿后的时间,从而减小时间差。
// --run--
document.body.innerHTML = "<div id='countdown'></div>"
const interval = 1000;
const startTime = Date.now();
// 模拟服务器返回的剩余时间
let time = 600000;
let count = 0;
let timeCounter;
function createTime(diff) {
if (diff <= 0) {
document.getElementById("countdown").innerHTML = `<span>00时00分00秒</span>`;
} else {
const hour = Math.floor(diff / (60 * 60 * 1000));
const minute = Math.floor((diff - hour * 60 * 60 * 1000) / (60 * 1000));
const second = Math.floor((diff - hour * 60 * 60 * 1000 - minute * 60 * 1000) / 1000);
document.getElementById("countdown").innerHTML = `<span>${hour}时${minute >= 10 ? minute : `0${minute}`}分${second >= 10 ? second : `0${second}`}秒</span>`;
}
}
function countDown() {
count++;
const gap = Date.now() - (startTime + count * interval);
let nextTime = interval - gap;
if (nextTime < 0) {
nextTime = 0;
}
// time -= interval;
const remainTime = time - (Date.now() - startTime);
console.log(`误差:${gap} ms,下一次执行:${nextTime} ms 后,离活动开始还有:${remainTime} ms`);
createTime(remainTime);
clearTimeout(timeCounter);
timeCounter = setTimeout(countDown, nextTime);
}
createTime(time);
timeCounter = setTimeout(countDown, interval);
// 误差:8 ms,下一次执行:992 ms 后,离活动开始还有:58992 ms
// 误差:13 ms,下一次执行:987 ms 后,离活动开始还有:57987 ms
// 误差:12 ms,下一次执行:988 ms 后,离活动开始还有:56988 ms
// 误差:12 ms,下一次执行:988 ms 后,离活动开始还有:55988 ms
// tab切到后台
// 误差:227 ms,下一次执行:773 ms 后,离活动开始还有:54773 ms
// 误差:491 ms,下一次执行:509 ms 后,离活动开始还有:53509 ms
// 误差:392 ms,下一次执行:608 ms 后,离活动开始还有:52607 ms
// 误差:281 ms,下一次执行:719 ms 后,离活动开始还有:51719 ms
// 误差:438 ms,下一次执行:562 ms 后,离活动开始还有:50562 ms
// tab切回前台
// 误差:13 ms,下一次执行:987 ms 后,离活动开始还有:49987 ms
// 误差:13 ms,下一次执行:987 ms 后,离活动开始还有:48987 ms
// 误差:10 ms,下一次执行:990 ms 后,离活动开始还有:47990 ms
// ...
剩余时间不能按照正常的间隔时间累减(time -= interval
)
- 每次执行时间大于设置的间隔时间,存在误差;
- 当
tab
页面切到后台,实际执行的间隔时间大于1000ms;
不管执行的间隔时间如何变化,只要准确计算出每次的剩余时间(time - (Date.now() - startTime)
)就可以得到精准的倒计时。
可以看到每次会对误差做时间补偿,并且精准计算剩余时间,几乎是没有误差的
但是,还有一种特殊情况需要考虑,假如倒计时正在准确计时中,突然某刻有一个长任务(执行时间5000ms
)进入队列,当长任务进入调用栈执行时就会堵塞倒计时任务的执行,我们就会看到一个现象,计时停滞了(假设在01时36分55秒
),待到长任务执行完后,计时任务才进入调用栈执行,会看到倒计时从01时36分55秒
跳到01时36分50秒
开始计时。
加上一个长任务:
const longTask = () => {
const startTime = Date.now();
while(Date.now() - startTime < 5000) {}
}
longTask();
更快的异步执行
不是为了「动画」,而是单纯的希望最快速的执行异步回调:
使用异步函数:setTimeout、raf、setImmediate
:
1、setTimeout
会有「时间精度问题」
// --run--
var now = function(){
return performance ? performance.now() : +new Date();
};
var i = now();
setTimeout(function(){
setTimeout(function(){
console.log(now()-j);
},0);
var j = now();
console.log(j-i);
},0);
// 0.3999999910593033
// 1.20000000298023224
2、rAF
会跟随浏览器内置重绘页面的频率,约60Hz
,chrome
上测试:第一次时间多在1ms
内,第二次调用时间大于10ms
。
// --run--
var now = function(){
return performance ? performance.now() : +new Date();
};
var i = now();
requestAnimationFrame(function(){
requestAnimationFrame(function(){
console.log(now()-j);
});
var j = now();
console.log(j-i);
});
// 0.5
// 13
3、setImmediate
:仅IE10
支持,尚未成为标准。但NodeJS
已经支持并推荐使用此方法。另外,github
上有setImmediate.js
项目,用其它方法实现了setImmediate
功能。
4、postMessage
onmessage
:和iframe
通信时常常会使用到onmessage
方法,但是如果同一个window postMessage
给自身,其实也相当于异步执行了一个function
。
// --run--
var doSth = function(){};
window.addEventListener("message", doSth, true);
window.postMessage("", "*");
5、另外,还可以利用script
标签,实现函数异步执行(把script
添加到文档也会执行onreadystatechange
但是该方法只能在IE
下浏览器里使用),例如:
var newScript = document.createElement("script");
var explorer = window.navigator.userAgent;
if (explorer.indexOf('MSIE') >= 0) {
// ie
script.onreadystatechange = doSth;
} else {
// chrome
script.onload = doSth;
}
document.documentElement.appendChild(newScript);
理论上,执行回调函数的等待时间排序:setImmediate < readystatechange < onmessage < setTimeout 0 < requestAnimationFrame
另外,在「setImmediate.js项目」中说了它的实现策略,对上文进行一个有力的补充:
## The Tricks
### `process.nextTick`
In Node.js versions below 0.9, `setImmediate` is not available, but [`process.nextTick`][nextTick] is—and in those versions, `process.nextTick` uses macrotask semantics. So, we use it to shim support for a global `setImmediate`.
In Node.js 0.9 and above, `process.nextTick` moved to microtask semantics, but `setImmediate` was introduced with macrotask semantics, so there's no need to polyfill anything.
Note that we check for *actual* Node.js environments, not emulated ones like those produced by browserify or similar. Such emulated environments often already include a `process.nextTick` shim that's not as browser-compatible as setImmediate.js.
### `postMessage`
In Firefox 3+, Internet Explorer 9+, all modern WebKit browsers, and Opera 9.5+, [`postMessage`][postMessage] is available and provides a good way to queue tasks on the event loop. It's quite the abuse, using a cross-document messaging protocol within the same document simply to get access to the event loop task queue, but until there are native implementations, this is the best option.
Note that Internet Explorer 8 includes a synchronous version of `postMessage`. We detect this, or any other such synchronous implementation, and fall back to another trick.
### `MessageChannel`
Unfortunately, `postMessage` has completely different semantics inside web workers, and so cannot be used there. So we turn to [`MessageChannel`][MessageChannel], which has worse browser support, but does work inside a web worker.
### `<script> onreadystatechange`
For our last trick, we pull something out to make things fast in Internet Explorer versions 6 through 8: namely, creating a `<script>` element and firing our calls in its `onreadystatechange` event. This does execute in a future turn of the event loop, and is also faster than `setTimeout(…, 0)`, so hey, why not?
参考
JS中的事件循环与定时器
setTimeout 和 setInterval,你们两位同学注意点时间~
JavaScript动画实现原理
How to Get Accurate Countdown in JavaScript
setTimeout() 全局函数
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。