模擬實現 JS 引擎:深入瞭解 JS機制 以及 Microtask and Macrotask

一份炒飯發表於2019-01-17

如果JavaScript是單執行緒的,那麼我們如何像在Java中那樣建立和執行執行緒?

很簡單,我們使用events或設定一段程式碼在給定時間執行,這種非同步性在 JavaScript 中稱為 event loop

在這篇文章中,主要想分析兩個點:

  • Javascript 中的 event loop 系統是如何工作;
  • 實現自定義 Javascript 引擎來解釋 event loop 系統的工作原理並演示其任務佇列、執行週期。

JavaScript 中的 Event Loop 機制

JavaScript 是由 Stack 棧、Heap 堆、Task Queue 任務佇列組成的:

  • Stack:用來是一種類似於陣列的結構,用於跟蹤當前正在執行的函式;
  • Heap :用來分配 new 建立的物件;
  • Task Queue :是用來處理非同步任務的,當該任務完成時,會指定對應回撥進入佇列。

執行以下同步任務時

console.log('script start');
console.log('script end');
複製程式碼

JavaScript 會依次執行程式碼,首先執行該指令碼,具體分為以下幾步

  1. 獲取該指令碼、或輸入檔案的內容 ;

  2. 將上述內容包裹在函式內;

  3. 作為與程式關聯的“start”或“launch”事件的事件處理程式;

  4. 執行其他初始化;

  5. 發出程式啟動事件;

  6. 事件被新增到事件佇列中;

  7. Javascript引擎將該事件從佇列中拉出並執行註冊的處理程式,然後執行!— “Asynchronous Programming in Javascript CSCI 5828: Foundations of Software Engineering Lectures 18–10/20/2016” by Kenneth M. Anderson

總結一下就是,Javascript 引擎會將指令碼內容包裹在 Main 函式內,並將其關聯為程式 startlaunch 事件的對應處理程式,然後 Main 函式進入 Stack ,然後遇到 console.log('script start') ,將其入棧,輸出 log('script start'),待其執行完畢之後出棧,直到所有程式碼執行完。

模擬實現 JS 引擎:深入瞭解 JS機制 以及 Microtask and Macrotask

如果存在非同步任務時

console.log('script start');
setTimeout(function callback() {
    console.log('setTimeout');
}, 0);
console.log('script end');
複製程式碼

第一步,同上圖,執行 console.log('script start'),然後遇到**WebAPIs **(DOMajaxsetTimeout

模擬實現 JS 引擎:深入瞭解 JS機制 以及 Microtask and Macrotask

執行setTimeout(function callback() {}) 得到結果是在得到一個 Timer ,繼續執行 console.log('end')

此時如果 timer 執行完成,會讓其對應 callback 進入Task Queue

模擬實現 JS 引擎:深入瞭解 JS機制 以及 Microtask and Macrotask

然後當 Stack 中函式全部執行完成之後(也就是 Event Loop 的關鍵:如果 Stack 為空的話,按照先入先出的順序讀取 Task Queue 裡面的任務),將 callback 推入 Stack 中執行。

模擬實現 JS 引擎:深入瞭解 JS機制 以及 Microtask and Macrotask

所以上述程式碼的結果如下

console.log('script start');
setTimeout(function callback() {
	console.log('setTimeout');
}, 0);
console.log('script end');
// log script start
// log script end
// setTimeout
複製程式碼

以上是遊覽器利用 Event Loop 執行非同步任務時的機制。

Microtask 和 Macrotask 以及實現 JS 引擎

Microtask 以及 Macrotask 都屬於非同步任務,它們各自包括如下api:

  • Microtask:process.nextTickPromisesMutationObserver
  • Macrotask:setTimeoutsetIntervalsetImmediate 等。

其中 Macrotask 佇列就是任務佇列,而 Microtasks 則通常安排在當前正在執行的同步任務之後執行,並且需要與當前佇列中所有 Microtask 都在同一週期內處理,具體如下

for (macroTask of macroTaskQueue) {
    // 1. 處理 macroTask
    handleMacroTask();
      
    // 2. 處理當前 microTaskQueue 所有 microTask
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}
複製程式碼

執行如下程式碼

// 1. 首先進入 Stack log "script start"
console.log("script start");
// 2. 執行webAPi,完成後 anonymous function 進入 task queue
setTimeout(function() { 
    console.log("setTimeout");
}, 0);
new Promise(function(resolve) {
    // 3. 立即執行 log "promise1"
    console.log("promise1");
    resolve();
}).then(function() {
    // 4. microTask 安排在當前正在執行的同步任務之後
    console.log("promise2");
}).then(function() {
    // 5. 同上 
    console.log("promise3");
});
// 6. log "script end"
console.log("script end");
/*
script start
promise1
script end
promise2
promise3
setTimeout
*/
複製程式碼

所以輸出結果是 1 -> 3 -> 6 -> 4 -> 5 -> 2。

接下來,利用 Javascript模擬 JS Engine,這一部分可以優先檢視Microtask and Macrotask: A Hands-on Approach,這篇文章,然後來給如下程式碼挑錯。

首先在 JSEngine 內部維護巨集任務、微任務兩個佇列macroTaskQueuemicroTaskQueue 以及對應的 jsStack 執行棧,並定義相關操作。

class JsEngine {
      macroTaskQueue = [];
      microTaskQueue = [];
      jsStack = [];

      setMicro(task) {
        this.microTaskQueue.push(task);
      }
      setMacro(task) {
        this.macroTaskQueue.push(task);
      }
      setStack(task) {
        this.jsStack.push(task);
      }
	  setTimeout(task, milli) {
        this.macroTaskQueue.push(task);
      }
}
複製程式碼

接下來定義相關執行機制以及初始化操作

class JsEngine {
    ...
    // 與event-loop中的初始化對應
    constructor(tasks) {
        this.jsStack = tasks;
        this.runScript(this.runScriptHandler);
    }
    runScript(task) {
    	this.macroTaskQueue.push(task);
    }
	runScriptHandler = () => {
        let curTask = this.jsStack.shift();
        while (curTask) {
          	this.runTask(curTask);
          	curTask = this.jsStack.shift();
        }
    }
    runMacroTask() {
        const { microTaskQueue, macroTaskQueue } = this;
		// 根據上述規律,定義macroTaskQueue與microTaskQueue執行的先後順序
        macroTaskQueue.forEach(macrotask => {
        	macrotask();
          	if (microTaskQueue.length) {
            	let curMicroTask = microTaskQueue.pop();
            	while (curMicroTask) {
              		this.runTask(microTaskQueue);
             		curMicroTask = microTaskQueue.pop();
            	}
        	}
        });
    }
	// 執行task
    runTask(task) {
    	new Function(task)();
    }
}
複製程式碼

利用上述 Js Engine 執行如下程式碼

const scriptTasks = [
      `console.log('start')`,
      `console.log("Hi, I'm running in a custom JS 	engine")`,
      `console.log('end')`
    ];
const customJsEngine = new JsEngine(scriptTasks);
customJsEngine.setTimeout(() => console.log("setTimeout"));
customJsEngine.setMicro(`console.log('Micro1')`);
customJsEngine.setMicro(`console.log('Micro2')`);
customJsEngine.runMacroTask();
複製程式碼

最終得到結果

start
Hi, I'm running in a custom JS engine
end
Micro1
setTimeout
複製程式碼

總結

查了些資料,翻了一些視訊,把這個上述問題重新梳理了一下。

參考

相關文章