若是JavaScript是單線程的,那麼咱們如何像在Java中那樣建立和運行線程?web
很簡單,咱們使用
events
或設定一段代碼在給定時間執行,這種異步性在 JavaScript 中稱爲event loop
。ajax
在這篇文章中,主要想分析兩個點:api
JavaScript 是由 Stack 棧、Heap 堆、Task Queue 任務隊列組成的:數組
new
建立的對象;運行如下同步任務時promise
console.log('script start');
console.log('script end');
複製代碼
JavaScript 會依次執行代碼,首先執行該腳本,具體分爲如下幾步app
獲取該腳本、或輸入文件的內容 ;異步
將上述內容包裹在函數內;函數
做爲與程序關聯的「start」或「launch」事件的事件處理程序;oop
執行其餘初始化;ui
發出程序啓動事件;
事件被添加到事件隊列中;
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 都屬於異步任務,它們各自包括以下api:
process.nextTick
,Promises
,MutationObserver
;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
複製代碼
查了些資料,翻了一些視頻,把這個上述問題從新梳理了一下。