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

若是JavaScript是單線程的,那麼咱們如何像在Java中那樣建立和運行線程?web

很簡單,咱們使用events或設定一段代碼在給定時間執行,這種異步性在 JavaScript 中稱爲 event loopajax

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

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

JavaScript 中的 Event Loop 機制

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

  • Stack:用來是一種相似於數組的結構,用於跟蹤當前正在執行的函數;
  • Heap :用來分配 new 建立的對象;
  • Task Queue :是用來處理異步任務的,當該任務完成時,會指定對應回調進入隊列。

運行如下同步任務時promise

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

JavaScript 會依次執行代碼,首先執行該腳本,具體分爲如下幾步app

  1. 獲取該腳本、或輸入文件的內容 ;異步

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

  3. 做爲與程序關聯的「start」或「launch」事件的事件處理程序;oop

  4. 執行其餘初始化;ui

  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'),待其運行完畢以後出棧,直到全部代碼運行完。

若是存在異步任務時

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

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

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

此時若是 timer 運行完成,會讓其對應 callback 進入Task Queue

而後當 Stack 中函數所有運行完成以後(也就是 Event Loop 的關鍵:若是 Stack 爲空的話,按照先入先出的順序讀取 Task Queue 裏面的任務),將 callback 推入 Stack 中執行。

因此上述代碼的結果以下

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
複製代碼

總結

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

參考

相關文章
相關標籤/搜索