1. 程式人生 > >JS-執行緒、事件迴圈、任務佇列

JS-執行緒、事件迴圈、任務佇列

JS 是單執行緒的,但是卻能執行非同步任務,這主要是因為 JS 中存在事件迴圈(Event Loop)和任務佇列(Task Queue)。

事件迴圈:

JS 會建立一個類似於 while (true) 的迴圈,每執行一次迴圈體的過程稱之為 Tick。每次 Tick 的過程就是檢視是否有待處理事件,如果有則取出相關事件及回撥函式放入執行棧中由主執行緒執行。待處理的事件會儲存在一個任務佇列中,也就是每次 Tick 會檢視任務佇列中是否有需要執行的任務。

任務佇列:

非同步操作會將相關回調新增到任務佇列中。而不同的非同步操作新增到任務佇列的時機也不同,如 onclick, setTimeout, ajax 處理的方式都不同,這些非同步操作是由瀏覽器核心的 webcore 來執行的,webcore 包含上圖中的3種 webAPI,分別是 DOM Binding、network、timer模組。

  1. onclick 由瀏覽器核心的 DOM Binding 模組來處理,當事件觸發的時候,回撥函式會立即新增到任務佇列中。
  2. setTimeout 會由瀏覽器核心的 timer 模組來進行延時處理,當時間到達的時候,才會將回調函式新增到任務佇列中。
  3. ajax 則會由瀏覽器核心的 network 模組來處理,在網路請求完成返回之後,才將回撥新增到任務佇列中。

主執行緒

JS只有一個執行緒,稱之為主執行緒。而事件迴圈是主執行緒中執行棧裡的程式碼執行完畢之後,才開始執行的。所以,主執行緒中要執行的程式碼時間過長,會阻塞事件迴圈的執行,也就會阻塞非同步操作的執行。只有當主執行緒中執行棧為空的時候(即同步程式碼執行完後),才會進行事件迴圈來觀察要執行的事件回撥,當事件迴圈檢測到任務佇列中有事件就取出相關回調放入執行棧中由主執行緒執行。

  • (所有程式碼都主執行緒裡跑,主執行緒沒程式碼了就在任務佇列中讀回撥函式(非同步操作)回到主執行緒裡跑)
  1. 所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
  2. 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
  4. 主執行緒不斷重複上面的第三步。("任務佇列"是一個先進先出的資料結構,排在前面的事件,優先被主執行緒讀取。)
例1:
var req = new XMLHttpRequest();
  req.open('GET', url);    
  req.onload = function (){};    // 這兩個非同步方法就會在 ajax 完成後推入任務佇列,再由主執行緒執行
  req.onerror = function (){};    
  req.send();

例2:
setTimeout(function(){
  // 如果有大量的操作,可能會阻塞 UI 等,則可以使用 setTimeout 讓這些操作在主執行緒把更重要的程式碼執行完畢之後,再來執行這裡的操作。從而提高瀏覽器的效能。
},0);// 設定為 0,也會有個最小間隔值,也會在主執行緒中的程式碼執行完成後,由事件迴圈從任務佇列將回調新增到執行棧中才執行


例3:
// 事件迴圈測試。執行結果是 2-3-4-1,1在最後輸出,說明事件迴圈是所有同步程式碼執行完後才開始執行的。

'use strict';

setTimeout(function() {
  console.log(1);
}, 0);

console.log(2);

let end = Date.now() + 1000*5;

while (Date.now() < end) {
}

console.log(3);

end = Date.now() + 1000*5;

while (Date.now() < end) {
}

console.log(4);