5
头图

计时器

计时器在前端有很多应用场景,比如电商业务中秒杀和抢购活动的倒计时。在探讨计时器之前先来回顾下它们的基本概念:

基本定义与用法

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

setTimeoutsetInterval 实现原理一致,setTimeout(fn,0) 会将「事件」放入task queue的尾部,在下一次loop中,当「同步任务」与task queue中现有事件都执行完之后再执行。

setInterval

setInterval如果使用「固定步长」(间隔时间为定值),指的是向队列添加新任务之前等待的时间。比如,调用 setInterval()的时间为 01:00:00,间隔时间为 3000 毫秒。这意味着 01:00:03 时,浏览器会把「任务」添加到「执行队列」。浏览器不关心这个「任务」什么时候执行或者执行要花多长时间。因此,到了 01:00:06,它会再向「队列」中添加一个「任务」。由此可看出,执行时间短、非阻塞的回调函数比较适合 setInterval()

时间精度

setTimeoutsetInterval都存在「时间精度问题」,至少在4ms以上(4ms 一次 event loop,也即是最少4ms才检查一次setTimeout的时间是否达到),根据浏览器、设备是否插电源等有所不同,最多能达到近16ms。为了解决这个问题,加快响应速度,产生了「setImmediate APIsetImmediate.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();

img-20230511114208.png

// --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();

img-20230512113933.png

事件积压

如果主线程(或执行栈)中的任务与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时长

img-20230510173755.png

当主线程或者定时器回调函数执行时长越长,「事件积压」就越严重。为了避免长时间运行产生大量「积压」在内存中待执行的函数,产生性能损耗,现在浏览器会保证「当任务队列中没有定时器的任何其它代码实例时,才将新的定时器添加到任务队列」。

img-20230510174807.png

事件积压解决方案

如果有些浏览器没有做此优化,一定要使用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);

img-20230524180030.gif
原理是每隔一定时间(10ms)增加1px,一直到200px为止。然而,动画结束后显示的耗时却不止1000ms,有1011ms。究其原因,是因为setInterval并不能严格保证执行间隔。

img-20230525192713.png

有没有更好的做法呢?下面先来看一道小学数学题:

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);

img-20230525093929.gif

这样改良之后,可以看到动画执行耗时最多只会有几毫秒的误差。但是问题还没完全解决,在浏览器开发工具中检查test2元素可以发现,test2的最终宽度可能不止200px。仔细检查animate2函数的代码可以发现:

  • percentage的值可能大于1,可以通过Math.min限制最大值解决。
  • 即使保证了percentage的值不大于1,只要endValuestartValue为小数,(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执行动画

举一个例子来思考下,愤怒的小鸟游戏中,小鸟飞过屏幕时,用户应该在每次屏幕刷新时体验到小鸟以相同的速度前进。假设显示器刷新频率60Hz16又2/3毫秒渲染一次),屏幕将在以下时间(以毫秒为单位)更新:0、16又2/333又1/35066又2/383又1/3100等。再假设定时器固定步长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

img-20230511164712.png

在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpugpu和内存使用量。

// --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);
}

img-20230511171944.png

setTimeout相比,requestAnimationFrame不是自己指定回调函数运行的时间,而是跟着浏览器内建的刷新频率来执行回调,这当然就能达到浏览器所能实现动画的最佳效果了。

但另外一方面,requestAnimationFrame的预期执行时间要比setTimeout要长,因为setTimeout的最小执行时间是由「浏览器的时间精度」决定的,但raf会跟随浏览器DOM的刷新频率来执行,理论为16又2/3ms。但是,在setTimeout中如果进行了DOM操作(尤其是产生了重绘)通常不会立即执行,而是等待浏览器内建刷新时才执行。因此对于「动画」来说的话,raf要远远比setTimeout适合得多。

rAFsetTimeout性能比较:(据某些人说,早期的raf性能堪忧,尤其是在手机上,反而不如setTimeout
MacBook Pro Chrome 112.0.5615.137(正式版本) (arm64):

  • setTimeout用时:30947ms
  • rAF用时: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();

img-20230525104124.gif

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();

img-20230525104218.gif

由于requestAnimationFrame的特性之一:会把每一帧中的所有DOM操作集中起来,在「一次重绘或回流中就完成」,因此有github项目fastdom

后台最小超时延迟

为了优化后台标签的加载损耗(以及降低耗电量),浏览器会在「非活动标签」中强制执行一个「最小的超时延迟」。如果一个页面正在使用网络音频 API AudioContext 播放声音,也可以不执行该延迟。

这方面的具体情况与浏览器有关:

  • Firefox 桌面版和 Chrome 针对不活动标签都有一个 「1 秒的最小超时值」。
  • 安卓版 Firefox 浏览器对不活动的标签有一个至少 15 分钟的超时,并可能完全卸载它们。
  • 如果标签中包含 AudioContextFirefox 不会对非活动标签进行节流。
// --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);

img-20230511110059.png

倒计时

通常电商业务都会有倒计时功能的秒杀和抢购活动,实现倒计时功能一般会从服务端获取剩余时间来计算,每走动一秒就刷新倒计时显示。

使用 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,然而事实并不是这样,而是存在着误差:

img-20230424112953.png

使用 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 需要在同步代码执行完成后才重新开始计时):

img-20230424113440.png

为什么会存在误差?

这里涉及到 JS 的代码执行顺序问题, JS 属于单线程,代码执行的时候首先是执行主线程的任务,也就是同步的代码,如果遇到异步的代码块,并不会立即执行,而是丢进任务队列中,任务队列是先进先出,待主线程的代码执行完毕以后,才会依次的执行任务队列中的函数。所以,计时器函数实际执行时间一定大于指定的时间间隔。
img-20230525110021.png

因此,对于 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保证后续的定时时间为补偿后的时间,从而减小时间差。

img-20230525143605.png

// --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();

img-20230525180713.gif

更快的异步执行

不是为了「动画」,而是单纯的希望最快速的执行异步回调:

使用异步函数: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会跟随浏览器内置重绘页面的频率,约60Hzchrome上测试:第一次时间多在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() 全局函数


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。


引用和评论

0 条评论