1. 程式人生 > 其它 >你不知道的Javascript:有趣的setTimeout

你不知道的Javascript:有趣的setTimeout

有時候,小小的細節往往隱藏著大大的智慧

今天在回顧JavaScript進階用法的時候,發現一個有趣的問題,話不多說,先上程式碼:

for(var j=0;j<10;j++){
  setTimeout(function(){console.log(i)},5000)
}

看到這三行程式碼,也許你會不耐煩道:又要講閉包?要吐了好麼?別急,讓我們先來思考一下,這段程式碼在瀏覽器中的執行結果是什麼?

  • 甲:順序列印0到9?
  • 乙:這題我見過,列印十個10!

哪個答案正確?我們繼續上圖:

執行結果顯示,瀏覽器打印出了十個10(因為圖片處理的原因,按下回車到列印之前其實間隔了5秒左右),貌似乙勝出了。但如果你足夠細心,你會發現幾個問題:

  1. 為什麼會迴圈列印十個10而不是0到9?
  2. 從結果來看,for迴圈執行完跳出之後,才開始執行setTimeout(所以j才等於10),為什麼不是每次迭代都執行一次setTimeout呢?

如果上述兩個問題你都能回答上來,恭喜你,你已經開始掌握了JavaScript深層次的知識,如果不能,那就乖乖往下看吧!

為什麼會迴圈列印十個10

許多人習慣用第二個問題中的執行結果來回答這個問題:for迴圈執行完跳出之後,才開始執行setTimeout,所以才打印了十個10。這樣的答案,只能說是既應付了自己,又應付了別人。其實,要解答第一個問題,首先要解答的就是第二個問題。

為什麼不是每次迭代都執行一次setTimeout

大家都知道,JavaScript在ES6出現以前,是沒有塊狀作用域的,這就意味著, 在for迴圈中用var定義的變數j,其實是屬於全域性的,即在全域性範圍內都可以被訪問到,既然如此,那其實整個全域性作用域中就只有一個j,每次for迴圈都是在更新這個j

那麼現在關鍵的問題在於,為什麼整個for迴圈會先於setTimeout執行,而不是我們正常理解的,一次迭代執行一次。

這就涉及到了JavaScript的核心特性:單執行緒

JavaScript設計的初衷,是瀏覽器用來與使用者進行互動和DOM操作的。這就決定了它必須是單執行緒的,設想JavaScript同事有兩個執行緒,一個執行緒在DOM節點新增內容,一個執行緒刪除該節點,瀏覽器就會出現混亂。所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。

單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。

為了優化單執行緒的效能,JavaScript將任務分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)

  • 同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務.
  • 非同步任務指的是,不進入主執行緒,而進入任務佇列(task queue)的任務,只有主執行緒中的同步任務執行完畢,非同步任務才會進入執行佇列執行。只要主執行緒空了,就會去讀取"任務佇列",這就是JavaScript的執行機制。這個過程會不斷重複。

setTimeout,就被JavaScript定義為非同步任務。每次for迴圈的迭代,都將setTimeout中的回撥函式加入任務佇列等待執行。也就是說,只有同步任務中的for迴圈完全結束,主執行緒中才會去任務佇列中找到尚未執行的十個setTimeout(十次迭代)回撥函式並順序執行(先進先出)。而此時,i已經經過迴圈結束變成了10,所以,此時主執行緒執行的,是十個一模一樣的列印j的回撥函式,即列印十個10。至此就完美回答了第一和第二個問題。文章開頭的程式碼與下面的程式碼其實是等價的:

for(var i=0;i<10;i++){}
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)
setTimeout(console.log(i),5000)

小小的一個setTimeout,牽扯出了很多JavaScript的深層次問題,雖然總結成一篇文章只有區區數百字,但是我在成文的過程中查閱了大量的資料,也做了許多實驗。

最後,給出一個很小但是仍然在困擾我的一個問題,希望有興趣的小夥伴可以跟我一起研究:

setTimeout(function(){while(true){}},6000);
setTimeout(function(){console.log(1)},10000);
setTimeout(function(){console.log(2)},5000);

上述程式碼的執行順序是怎樣的?setTimeout的定時,是定時插入執行棧之後立即執行,還是立即插入執行棧定時執行?

期待大家的留言。