如果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 會依次執行程式碼,首先執行該指令碼,具體分為以下幾步
獲取該指令碼、或輸入檔案的內容 ;
將上述內容包裹在函式內;
作為與程式關聯的“start”或“launch”事件的事件處理程式;
執行其他初始化;
發出程式啟動事件;
事件被新增到事件佇列中;
Javascript引擎將該事件從佇列中拉出並執行註冊的處理程式,然後執行!— “Asynchronous Programming in Javascript CSCI 5828: Foundations of Software Engineering Lectures 18–10/20/2016” by Kenneth M. Anderson
總結一下就是,Javascript 引擎會將指令碼內容包裹在 Main
函式內,並將其關聯為程式 start
或 launch
事件的對應處理程式,然後 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 **(DOM
,ajax
,setTimeout
)
執行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.nextTick
,Promises
,MutationObserver
; - Macrotask:
setTimeout
,setInterval
,setImmediate
等。
其中 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
內部維護巨集任務、微任務兩個佇列macroTaskQueue
,microTaskQueue
以及對應的 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
複製程式碼
總結
查了些資料,翻了一些視訊,把這個上述問題重新梳理了一下。