博客文章地址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
, 關於這個問題也有不少討論,仍是有很多人想把它改會咱們熟悉的方式的
具體最後要怎樣仍是要看後面的版本修改了。