博客文章地址javascript
setTimeout
和 setInterval
是咱们在 javaScript
中常常用到的定时器,setTimeout
方法用于在指定的毫秒数后调用函数或计算表达式,setInterval
可按照指定的周期不停的调用函数或计算表达式。java
可是当咱们要循环调用某任务时候,处了用 setInterval
指定周期外,咱们也能够用函数中嵌套setTimeout
回掉本身来实现, 能够看下面一段代码node
// A function myTimeout() { doStuff() setTimeout(myTimeout, 1000) } myTimeout() // B function myTimeout() { doStuff() } myTimeout() setInterval(myTimeout, 1000)
上面A
, B
两个方法都是在循环执行 myTimeout
函数,但是它们之间有什么不一样呢。咱们大部分都知道这其实取决与 doStuff
所消耗的时间, 以下图所示若是 doStuff
消耗时间很短(实际中大部分消耗时间都很短很难有所察觉),两个方法效果近似git
当doStuff
是一个很复杂的计算,须要消耗很长时间时候,咱们就能够分析出A
方法(用setTimeout回掉)可以保障每一次任务结束到下一次任务开始的时间间隔为咱们预期的值,可是B
(setInterval)却能保证任务开始到下一次任务开始之间的间隔为咱们预期的值,(固然若是doStuff
执行时间比咱们预期间隔还长,setInterval
还有可能会直接放弃某次任务,这种罕见状况咱们暂不考虑)github
为了感觉其中的差别,这里定义一个模拟任务执行的函数segmentfault
function wait(time) { var start = Date.now() while(Date.now() - start < time){} }
wait
什么也没作,可是却能够阻塞进程time
毫秒的时间,而后咱们定义 doStuff
,让它每次执行阻塞进程500ms
,并且能够输出间隔时间信息,以及本次执行结束到下次执行开始的时间间隔promise
function doStuff() { console.log('doStuff___start', new Date().getSeconds()) //每次输出当前的秒数 console.timeEnd('timeout') //每次输出此次执行与上一次执行结束的时间间隔 wait(500) console.time('timeout') }
而后咱们分别运行A
, B
两种方法app
/* * 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(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
, 关于这个问题也有不少讨论,仍是有很多人想把它改会咱们熟悉的方式的
具体最后要怎样仍是要看后面的版本修改了。