從setTimeout說起
眾所周知,JavaScript是單執行緒的程式設計,什麼是單執行緒,就是說同一時間JavaScript只能執行一段程式碼,如果這段程式碼要執行很長時間,那麼之後的程式碼只能盡情地等待它執行完才能有機會執行,不像人一樣,人是多執行緒的,所以你可以一邊觀看某島國動作片,一邊盡情揮灑汗水。JavaScript單執行緒機制也是迫不得已,假設有多個執行緒,同時修改某個dom元素,那麼到底是聽哪個執行緒的呢?
既然已經明確JavaScript是單執行緒的語言,於是我們想方設法要想出JavaScript的非同步方案也就可以理解了。比如執行到某段程式碼,需求是1000ms後呼叫方法A,JavaScript沒有sleep函式能掛起執行緒一秒啊?如何能夠使得程式碼做到一邊等待A方法執行,一邊繼續執行下面的程式碼,彷彿開了兩個執行緒一般?機制的科學家們想出了setTimeout方法。
setTimeout方法想必大家都已經很熟悉了,那麼setTimeout(function(){..}, a)真的是ams後執行對應的回撥嗎?
1 2 3 4 5 |
setTimeout(function() { console.log('hello world'); }, 1000); while(true) {}; |
1s中之後,控制檯並沒有像預料中的一樣輸出字串,而網頁標籤上的圈圈一直轉啊轉,掐指一算,可能陷入while(true){}的死迴圈中了,可是為什麼呢?雖然會陷入死迴圈可是也得先輸出字串啊!這就要扯到JavaScript執行機制了。
JavaScript執行機制
一段JavaScript程式碼到底是如何執行的?阮一峰老師有篇不錯的文章(JavaScript 執行機制詳解:再談Event Loop),我就不再重複造輪子了;如果覺得太長不看的話,樓主簡短地大白話描述下。一段js程式碼(裡面可能包含一些setTimeout、滑鼠點選、ajax等事件),從上到下開始執行,遇到setTimeout、滑鼠點選等事件,非同步執行它們,此時並不會影響程式碼主體繼續往下執行(當執行緒中沒有執行任何同步程式碼的前提下才會執行非同步程式碼),一旦非同步事件執行完,回撥函式返回,將它們按次序加到執行佇列中,這時要注意了,如果主體程式碼沒有執行完的話,是永遠也不會觸發callback的,這也就是上面的一段程式碼導致瀏覽器假死的原因(主體程式碼中的while(true){}還沒執行完)。
網上還有一篇流傳甚廣的文章(猛戳How JavaScript Timers Work),文章裡有張很好的圖,我把它盜過來了。
文章裡沒有針對這幅圖的程式碼,為了能更好的說明流程,我嘗試著給出程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// some code setTimeout(function() { console.log('hello'); }, 10); // some code document.getElementById('btn').click(); // some code setInterval(function() { console.log('world'); }, 10); // some code |
我們開始執行程式碼。第一塊程式碼大概執行了18ms,也就是JavaScript的主體程式碼,在執行過程中,先觸發了一個setTimeout函式,程式碼繼續執行,只等10ms後響應setTimeout的回撥,接著是一個滑鼠點選事件,該事件有個回撥(或許是alert一些東西),不能立即執行(單執行緒),因為js主體程式碼還沒執行完,所以這個回撥被插入執行佇列中,等待執行;接著setInterval函式被執行,我們知道,此後每隔10ms都會有回撥(嘗試)插入佇列中,執行到第10ms的時候,setTimeout函式的回撥插入佇列。js函式主體執行完後,大概是18ms這個點,我們發現佇列中有個click的callback,還有個setTimeout的callback,於是我們先執行前者,在執行的過程中,setInterval的10ms響應時間也過了,同樣回撥被插入佇列。click的回撥執行完,執行setTimeout的回撥,這時又10ms過去了,setInterval又產生了回撥,但是這個回撥被拋棄了,之後發生的事大家都一目瞭然了。
這裡有一點我不太明白,就是關於interval回撥的drop。按照How JavaScript Timers Work裡的說法是,如果等待佇列裡已經有同一個interval函式的回撥了,將不會有相同的回撥插入等待佇列。
“Note that while mouse click handler is executing the first interval callback executes. As with the timer its handler is queued for later execution. However, note that when the interval is fired again (when the timer handler is executing) this time that handler execution is dropped. If you were to queue up all interval callbacks when a large block of code is executing the result would be a bunch of intervals executing with no delay between them, upon completion. Instead browsers tend to simply wait until no more interval handlers are queued (for the interval in question) before queuing more.”
查到一篇前輩的文章Javascript定時器學習筆記,裡面說“為了確保定時器程式碼插入到佇列總的最小間隔為指定時間。當使用setInterval()時,僅當沒有該定時器的任何其他程式碼例項時,才能將定時器程式碼新增到程式碼佇列中”。但是我自己實踐了下覺得可能並非如此:
1 2 3 4 5 6 7 8 9 |
var startTime = +new Date; var count = 0; var handle = setInterval(function() { console.log('hello world'); count++; if(count === 1000) clearInterval(handle); }, 10); while(+new Date - startTime < 10 * 1000) {}; |
按照上文的說法,由於while對執行緒的“阻塞”,使得相同的setInterval的回撥不能加在等待佇列中,但是實際在chrome和ff的控制檯都輸出了1000個hello world的字串,我也去原文博主的文章下留言詢問了下,暫時還沒答覆我;也可能是我對setInterval的認識的姿勢不對導致,如果有知道的朋友還望不吝賜教,萬分感激!
總之,定時器僅僅是在未來的某個時刻將程式碼新增到程式碼佇列中,執行時機是不能保證的。
setTimeout VS setInterval
以前看到過這樣的話,setInterval的功能都能用setTimeout去實現,想想也對,無窮盡地遞迴呼叫setTimeout不就是setInterval了嗎?
1 2 3 |
setInterval(function() { // some code }, 10); |
根據前文描述,我們大概懂了以上setInterval回撥函式的執行時間差<=10ms,因為可能會由於執行緒阻塞,使得一系列的回撥全部在排隊。用setTimeout實現的setInterval效果呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 1 function func() { setTimeout(function() { // some code func(); }, 10); } func(); // 2 setTimeout(function() { // some code setTimeout(arguments.callee, 1000); }, 10); |
很顯然兩個回撥之間的間隔是>10ms的,因為前面一個回撥在佇列中排隊,如果沒有等到,是不會執行下面的回撥的,而>10ms是因為回撥中的程式碼也要執行時間。換句話說,setInterval的回撥是並列的,前一個回撥(有沒有執行)並不會影響後一個回撥(插入佇列),而setTimeout之間的回撥是巢狀的,後一個回撥是前一個回撥的回撥(有點繞口令的意思)
參考資料
2015.7.1修正
經驗證,確實是樓主對於setInterval認識的姿勢有誤,也對得起兩個反對的差評,當使用setInterval()時,僅當沒有該定時器的任何其他程式碼例項時,才能將定時器程式碼新增到程式碼佇列中。
樓主的示例程式碼,正如評論中說的一樣,無論有無阻塞,都會執行1000次。程式碼修改如下:
1 2 3 4 5 6 7 |
var startTime = +new Date; var handle = setInterval(function() { console.log('hello world'); }, 3000); while(+new Date - startTime < 10 * 1000) {}; |
如果按照之前的認識,在while阻塞過程中,setInterval應該插入了3個回撥函式,而當while執行完後,控制檯應該打出連續3個字串,但是並沒有,說明確實只加入了一個回撥函式,其他兩個被drop了。而木的樹舉了個更好的例子,詳見Javascript定時器學習筆記評論部分的第二個示例程式碼。
所以確實在使用setInterval時:
- 某些間隔會被跳過
- 多個定時器的程式碼執行之間的間隔可能會比預期的小(當前的setInterval回撥正在執行,後一個新增)