一塊兒瞭解Javascript定時器

介紹

這篇文章會聊聊關於Javascript的定時器以及它的執行機制,首先會先翻譯一篇比較好的定時器和事件隊列的文章,而後會根據一些經典的例題來更深刻了解。javascript

瞭解定時器和事件隊列

此章節翻譯自原文java

在基礎層面上去了解 Javascript 定時器的工做原理是蠻重要的。由於單線程的問題不少時候他們的行爲都是無心義的。讓咱們先來看看構建和操做Timers的函數。瀏覽器

  • var id = setTimeout(fn,delay); 初始化一個單次定時器,它會在設置的延遲時間(delay)後執行特定的回調函數。這個函數會返回一個惟一的ID,這個ID能夠在以後用來取消這個定時器。
  • var id = setInterval(fn,delay);setTimeout 相似,可是區別在於這個函數會屢次執行直到它被中止。
  • clearInterval(id);clearTimeout(id); 傳入定時器的ID(ID由上面兩個函數返回)而後中止定時器回調。

爲了瞭解定時器內部的運行原理,有一個很重要的概念咱們須要探討: 定時器延遲是不能保證的。由於全部的JS腳本都是在瀏覽器的單個線程上執行,異步事件在被觸發的時候纔會執行(例如鼠標事件和定時器),這個圖能夠很好的說清楚。閉包

image_1

這張圖裏面有不少知識點須要消化,可是徹底理解了它就能夠更好的幫助咱們瞭解Javascript的異步事件機制的運做。這個圖是一維的:垂直量度標記的是時間,單位是ms,藍色框裏面的是正在執行的Javascript部分。例如第一個Javascript部分執行了大概18ms,鼠標點擊事件大概執行了11ms,以此類推。異步

由於Javascript只能一次執行一段代碼(由於它的單線程性質),全部的代碼塊都會在線程裏面堵塞其餘的代碼塊。這意味着當異步事件發生的時候(例如鼠標點擊事件,定時器觸發或者一個XMLHttpRequest完成),它將會進入事件隊列排隊等待執行(這種排隊實際上發生的狀況確定會因瀏覽器和瀏覽器之間的不一樣而有所不一樣,因此這裏是一個簡化的描述)。函數

首先,在第一個Javascript代碼塊中,兩個定時器被啓動:10ms的 setTimeout 和 10ms的 setInterval。定時器的啓動時間和位置是在咱們完成第一個代碼塊前就已經觸發了。可是請注意,它不會當即執行(因爲線程不能執行)。而是進入隊列中以便再下一個可用時間執行。【譯者補充:就是說會等第一代碼塊的順序代碼執行完後,回頭纔會去處理事件隊列裏面的代碼。】學習

另外,在第一個Javascript代碼塊中咱們還看到一個鼠標點擊事件發生。與這個事件關聯的異步回調函數也不會立刻就執行(由於咱們不會知道用戶什麼時候執行操做,所以這個時間也被認爲是異步的),跟 timeout 事件初始化同樣,它也會放入到隊列裏面稍後執行。線程

在初始化Javascript代碼塊執行完後瀏覽器立刻會問一個問題:「還有誰?!誰在等着被執行?!」 在這個圖的案例裏面,鼠標點擊回調事件和定時器的回調事件都在等着。瀏覽器會選擇下一個隊列事件(點擊回調事件)並當即執行。timeout 的回調時間則會等待下一個時機去執行。翻譯

注意,當鼠標點擊事件回調在執行的時候,第一個 interval 事件回調到達時間點執行,它會跟 timeout 的回調事件同樣進入隊列等待稍後執行。可是請注意,當 interval 再次被觸發(當 timeout 的回調事件還在執行)的時候,這個 interval 的回調執行會被放棄。假如在執行大塊代碼塊的時候對全部的 interval 回調進行排隊,那麼這一系列的回調之間將不會有延遲。但相反,瀏覽器事實上每每只是等待,在更多的其餘的事件入隊以前不會再有其餘的 interval 回調事件(指相同ID interval衍生出來的回調事件) 會被放入隊列。【譯者補充:就是說,當一個佔用時間較長的事件在執行的時候,若是隊列中已經有一個相同ID interval產生的且還沒執行的回調事件在,即便到達了再下一個得interval觸發時間也不會有新的interval回調事件入隊。】code

事實上咱們能夠看到案例中第三個 interval 回調事件被觸發的時候, 前一個 interval 回調事件正在執行。這告訴了咱們一個事實:interval 回調事件不會在乎什麼當前執行的內容,它們會不加區分地進入隊伍,即便回調事件之間的時間會被浪費掉。

最後在第二個 interval 回調事件執行完後,咱們能夠看到在javascipt引擎的隊列裏面已經沒有東西能夠執行了。這意味着瀏覽器如今會等待新的異步事件的發生。在 interval 回調事件再次觸發的時候時間線已經到達50ms了,這一次,由於沒有其餘代碼塊在執行,因此這個 interval 回調事件會立刻執行。

來看一個例子去更好地區分 setTimeoutsetInterval

setTimeout(function(){
  /* Some long block of code... */
  setTimeout(arguments.callee, 10);
}, 10);
 
setInterval(function(){
  /* Some long block of code... */
}, 10);

乍看之下,這兩個代碼片斷彷佛實現的功能是一致的,但仔細看其實不一樣。值得注意的是, 在前一個回調執行以後,setTimeout 的代碼至少會有10ms的延遲纔會執行(可能會更多,但不會更少)。 setInterval 則在每10ms的時間裏都會嘗試去執行回調,而不會管前一個回調執行的完成時間。

今天學習了不少,咱們來回顧一下:

  • Javascript 引擎只有一個單線程,強制異步事件排隊等待執行。
  • setTimeout 和 setInterval 在它們如何處理異步代碼之上有着根本性的不一樣
  • 若是定時器被阻止當即執行,它將被延遲到下一個可能的執行點(這將會比指望的延遲時間更長)。
  • 若是 Interval 的回調事件須要花費足夠長的時間執行,那麼它們將能夠無延遲的背靠背(連續)執行。

這些都是重要的基礎知識。瞭解Javascript引擎的工做原理,特別是遇到大量異步事件的狀況下,能夠在構建高級應用程序代碼的基礎層面上作好準備。

經典例題

for (var i=0; i<5; i++){
	console.log(i)
}

第一題就是基礎的不能再基礎了順序輸出0 1 2 3 4

for (var i=0; i<5; i++){
	setTimeout(function(){
		console.log(i)
	}, 1000 * i);
}

第二題就有咱們上一節提到的知識了,javascript會先把for循環執行完,把setTimeout的回調事件都放到事件隊列中,等初始塊代碼執行完後再去處理事件隊列裏的回調事件,而這個時候,for局部裏面的變量 i 已經一早遞加爲 5 了(注意,for循環是先執行語句3再去判斷能不能執行內部語句的,因此 i 已是 5 了)。所以結果是:

//延遲
5
5
5
5
5
for (var i=0; i<5; i++){
	(function(i){
		setTimeout(function(){
			console.log(i)
		}, 1000 * i);
	})(i);
}

第三題用了一個匿名函數和立刻執行的傳參來包住了setTimeout,就是說setTimeout 的 i 這個時候用的是閉包裏面的局部變量 i,由於匿名函數的 i 傳參不是引用傳值而是數值傳值,因此匿名函數裏的 i 不會根據外面for循環的變量 i 的改變而改變。所以結果是:

//延遲
0
1
2
3
4
for (var i=0; i<5; i++){
	(function(){
		setTimeout(function(){
			console.log(i)
		}, 1000 * i);
	})(i);
}

第四題考察的就是閉包的知識了,由於沒有值傳入,因此setTimeout讀的仍是已經跑完for循環的 i。所以結果是:

//延遲
5
5
5
5
5
for (var i=0; i<5; i++){
	setTimeout((function(i){
		console.log(i)
	})(i), 1000 * i);
}

第五題setTimeout裏面的回調函數被當即匿名調用了,因此先會跟正常輸出同樣,不會延遲執行。同時由於匿名函數沒有返回值,因此 setTimeout 的回調函數是 undefined。輸出結果是:

//無延遲
0
1
2
3
4
for (var i=0; i<5; i++){
	setTimeout((function(i){
		return function(){
			console.log(i);
		}
	})(i), 1000);
}

根據第五題的變形另外加一題,這題跟第五題的區別就是匿名函數有返回值,就是 setTimeout的回調函數就再也不是undefined了,因此輸出結果是:

//延遲
0
1
2
3
4

Reference

[0] How JavaScript Timers Work [1] 例題來源,小芋頭君知乎live(如禁止發佈請告知刪除)

相關文章
相關標籤/搜索