單線程的 JavaScript 一段一段地執行,前面的執行完了,再執行后面的,試想一個,如果前一個任務需要執行很久,比如接口請求、I/O 操作,此時后面的任務只能干巴巴地等待么?干等不僅浪費了資源,而且頁面的交互程度也很差。JavaScript 意識到了這個問題,他們將任務分成了同步任務和異步任務,對於二者有不同的處理。
JavaScript 在運行時會將變量存放在堆(heap)和棧(stack)中,堆中通常存放着一些對象,而變量及對象的指針則存放在棧中。JavaScript 在執行時,同步任務會排好隊,在主線程上按照順序執行,前面的執行完了再執行后面的,排隊的地方叫執行棧(execution context stack)。JavaScript 對異步任務不會停下來等待,而是將其掛起,繼續執行執行棧中的同步任務,當異步任務有返回結果時,異步任務會加入與執行棧不一樣的隊列,即任務隊列(task queue),所以任務隊列中存放的是異步任務執行完成后的結果,通常是回調函數。
當執行棧的同步任務已經執行完成,此時主線程閑下來,它便會去查看任務隊列是否有任務,如果有,主線程會將最先進入任務隊列的任務加入到執行棧中執行,執行棧中的任務執行完了之后,主線程便又去任務隊列中查看是否有任務可執行。主線程去任務隊列讀取任務到執行棧中去執行,這個過程是循環往復的,這便是 Event Loop,事件循環。
網上有張流傳甚廣的圖對這一過程進行了總結,在圖中我們可以看到,JavaScript 在運行時產生了堆和棧,ajax、setTimeout 等異步任務被掛起,異步任務的返回結果加入任務隊列,主線程會循環往復地讀取任務隊列中的任務,加入執行棧中執行。

為了更好的理解 JavaScript 的執行機制,我們來看個小例子。
1 console.log(1) 2 setTimeout(function() { 3 console.log(2) 4 }, 300) 5 console.log(3)
輸出的結果是 1,3,2。setTimeout 是一個定時器,延遲 300 毫秒執行,所以 300 毫秒后,打印 2 的回調函數才會進入任務隊列,等到執行棧中的代碼執行完成后,也就是打印出 1 和 3 后,打印出 2 的回調函數才進入執行棧執行。
如果將 setTimeout 的第二個參數設置為 0,它表示主線程空閑之后盡早執行它的回調,HTML5 規定 setTimeout 的第二個參數不得小於 4 毫秒。
1 setTimeout(function() { 2 console.log(1) 3 }, 0) 4 console.log(2) 5 6 // 2,1
對於 setTimeout 還有一個需要注意的是,它的延遲時間並不是等待多少毫秒后就一定會執行,始終是要等待主線程已經空閑了才會去讀取它,如果執行棧中的任務需要很長時間才能執行完,那任務隊列中的任務只能等待。我們可以通過一個例子來體驗一下。
1 var enterTime = Date.now() 2 3 function sleep(time) { 4 for(var temp = Date.now(); Date.now() - temp <= time;); 5 } 6 7 setTimeout(function() { 8 var exeTime = Date.now() 9 console.log(exeTime - enterTime) 10 }, 300) 11 12 sleep(1000) // 睡眠 1 秒
我們定義了一個 sleep 函數,設置了 1 秒的執行時間,所以 setTimeout 要等待的時間肯定大於 1 秒,而不是 300 毫秒后就執行了。上述代碼的執行結果是 1000 左右,值不固定,可以復制代碼到控制台執行看看。