setTimeout与setInterval
setTimeout
和 setInterval
是我们在 javaScript
中经常用到的定时器,setTimeout
方法用于在指定的毫秒数后调用函数或计算表达式,setInterval
可按照指定的周期不停的调用函数或计算表达式。
但是当我们要循环调用某任务时候,处了用 setInterval
指定周期外,我们也可以用函数中嵌套setTimeout
回掉自己来实现, 可以看下面一段代码
// A
function myTimeout() {
doStuff()
setTimeout(myTimeout, 1000)
}
myTimeout()
// B
function myTimeout() {
doStuff()
}
myTimeout()
setInterval(myTimeout, 1000)
上面A
, B
两个方法都是在循环执行 myTimeout
函数,可是它们之间有什么不同呢。我们大部分都知道这其实取决与 doStuff
所消耗的时间, 如下图所示如果 doStuff
消耗时间很短(实际中大部分消耗时间都很短很难有所察觉),两个方法效果近似
当doStuff
是一个很复杂的计算,需要消耗很长时间时候,我们就可以分析出A
方法(用setTimeout回掉)能够保障每一次任务结束到下一次任务开始的时间间隔为我们预期的值,但是B
(setInterval)却能保证任务开始到下一次任务开始之间的间隔为我们预期的值,(当然如果doStuff
执行时间比我们预期间隔还长,setInterval
还有可能会直接放弃某次任务,这种罕见情况我们暂不考虑)
为了感受其中的差异,这里定义一个模拟任务执行的函数
function wait(time) {
var start = Date.now()
while(Date.now() - start < time){}
}
wait
什么也没做,但是却可以阻塞进程time
毫秒的时间,然后我们定义 doStuff
,让它每次执行阻塞进程500ms
,而且可以输出间隔时间信息,以及本次执行结束到下次执行开始的时间间隔
function doStuff() {
console.log('doStuff___start', new Date().getSeconds()) //每次输出当前的秒数
console.timeEnd('timeout') //每次输出这次执行与上一次执行结束的时间间隔
wait(500)
console.time('timeout')
}
然后我们分别运行A
, B
两种方法
/*
* A方法 setTimeout
*/
// doStuff___start 36
// timeout: 1002.865966796875ms
// doStuff___start 37
// timeout: 1004.380859375ms
// doStuff___start 39
// timeout: 1001.550048828125ms
// doStuff___start 40
// timeout: 1001.051025390625ms
// doStuff___start 42
// timeout: 1001.637939453125ms
/*
* B方法 setInterval
*/
// doStuff___start 50
// timeout: 500.412109375ms
// doStuff___start 51
// timeout: 500.51806640625ms
// doStuff___start 52
// timeout: 500.099853515625ms
// doStuff___start 53
// timeout: 499.873291015625ms
// doStuff___start 54
// timeout: 500.439697265625ms
可以看到 A
方法(用setTimeout回掉),我们保证了每次进程结束到下一次进程开始的间隔为预期值,但是从每次进程开始的时间间隔(我们这里精确到了秒)是会改变的,而B
方法(setInterval)表现的和我们预期的相同,正好与A
相反。
nodejs中的差异
目前为止所以的表现都合理,至少很符合预期。可是当我在 nodejs(v8.1.4)
中测试时候,却发现不管我用 setTimeout
还是 setInterval
,他们总是能表现出同样的效果(都是上面A方法的效果【用setTimeout回掉】)。这一点让我很困惑,经过一番探究,在 nodejs
关于 timers
的代码中找到了答案。
nodejs
关于定时器的源码在 node/lib/timer 文件中,进入就关于定时器的一些设计解释,因为 node
是做服务端代码,在内部 TCP, I/O..
等大部分事件都会创建一个定时器,任何时间都可能存在大量的定时器任务,所以设计一个高效的定时器是很有必要的。
nodejs
实现定时器也很巧妙, 为了可以轻松取消添加事件,nodejs使用了双向链表将 timer
插入和移除操作复杂度降低,具体实现在 node/lib/internal/linkedlist.js 文件中, 链表缺点自然是去查找元素,但是node
,把同一个时间间隔的 timer
维护在同一个双向链表中,这样就不需要去查找,因为先插入的总是先执行,具体的分析可以参考这篇文章 通过源码解析 Node.js 中高效的 timer.
回归主题,在 nodejs
关于 timer
的源码下,我们可以找到执行定时器的代码
// setInterval 会返回 createRepeatTimeout 的返回值
exports.setInterval = function(callback, repeat, arg1, arg2, arg3) {
...
return createRepeatTimeout(callback, repeat, args);
}
// createRepeatTimeout函数生成timer
function createRepeatTimeout(callback, repeat, args) {
repeat *= 1; // coalesce to number or NaN
if (!(repeat >= 1 && repeat <= TIMEOUT_MAX))
repeat = 1; // 这里间隔如果小于1或者大于TIMEOUT_MAX(2^31-1)都会按照1计算
var timer = new Timeout(repeat, callback, args);
timer._repeat = repeat; // 追加了_repeat属性表示要循环调用
...
return timer;
}
// 函数回掉时,可以看到执行时在ontimeout函数中
function tryOnTimeout(timer, list) {
...
try {
ontimeout(timer);
threw = false;
} finally {
if (timerAsyncId !== null) {
if (!threw)
...
}
...
}
// ontimeout执行
function ontimeout(timer) {
var args = timer._timerArgs;
var callback = timer._onTimeout;
if (typeof callback !== 'function')
return promiseResolve(callback, args[0]);
if (!args)
timer._onTimeout();
else {
switch (args.length) {
case 1:
timer._onTimeout(args[0]);
break;
case 2:
timer._onTimeout(args[0], args[1]);
break;
case 3:
timer._onTimeout(args[0], args[1], args[2]);
break;
default:
Function.prototype.apply.call(callback, timer, args);
}
}
if (timer._repeat) // 追加timer
rearm(timer);
}
上面代码分析,可以看到追加循环调用是在 ontimeout
函数中,它里面一大堆判断参数个数的内容可以不管,最后的if(timer._repeat) rearm(timer)
判断是否要循环调用,可以看到它是在上面 timer._onTimeout
执行完之后才去执行的。这和我们开始写的A
方法(用setTimeout回掉)基本类似,至此在 nodejs
表现出的不同就可以理解了。
看 issues
, 关于这个问题也有很多讨论,还是有不少人想把它改会我们熟悉的方式的
- setTimeout interval should not include duration of callback
- setInterval interval includes duration of callback
具体最后要怎样还是要看后面的版本修改了。