總所周知 JS 運行在瀏覽器中,以單線程方式運行,每一個window一個JS線程。那麼瀏覽器是如何處理js中的I/O讀取、用戶點擊、setTimeout等異步事件,並使其餘js代碼不被阻塞的呢?html
瀏覽器中的事件循環就是其解決方式。簡單來講瀏覽器中的事件循環的機制是將產生的異步事件產生的回調暫時存儲在事件隊列中,等到合適的時機再去執行隊列中的異步事件的回調。web
要了解瀏覽器中的事件循環,須要先弄明白兩個重要的運行時概念。segmentfault
執行棧,函數調用時造成一個調用幀,並壓入棧中,當函數返回時,則幀彈棧。api
任務隊列,每個任務都包含一個處理該任務的函數,當任務產生時,任務及其處理函數會被做爲一個總體推入任務隊列中(例如:一個setTimeout,到達時間時,setTimeOut及其回調函數會做爲一個任務被推入任務隊列中)。任務隊列按照先進先出的順序執行。當任務隊列裏的任務須要被處理的時候(即調用任務的處理函數時),將會被移出隊列,調用其處理函數,此時造成一個調用幀,並壓入執行棧。promise
此時執行棧中的調用幀,直到執行棧爲空,而後再去處理隊列中的另外一個任務。瀏覽器
瀏覽器中的任務分爲兩種:task(macroTask 宏任務)和microtask(微任務)。不一樣的任務按照不一樣的規則執行。併發
一個事件循環裏有多個task queue,其中的包含多個任務,每一個任務嚴格的按照先進先出的順序執行。在一個task執行結束後下一個task執行以前,瀏覽器可對頁面進行從新渲染。 task queue中包含:app
一個事件循環中包含一個microTask queue。 microTask queue包含:webapp
一個任務完整的執行後,其餘任務纔會被執行。 即:執行棧中的調用幀,直到執行棧爲空,而後再去處理隊列中的另外一個任務。異步
在瀏覽器中,當事件發生而且該事件綁定了事件監聽時,該事件發生後的任務纔會被添加至隊列。
例如:爲一個DOM元素button綁定onclick一個處理事件,只有當button元素上的click事件發生時,該事件發生後的任務會被添加至隊列。
再例如: setTimeout 接受兩個參數:待加入隊列的任務和一個延遲。延遲表明了任務被添加至任務隊列的時間,只有通過了延遲的時間,該任務纔會被加入隊列。添加至隊列之後是否被處理,取決於隊列裏是否有其餘任務。所以延遲的時間表示最少延遲時間,而非確切的等待時間。
事件循環進程模型 步驟以下:
在事件循環中,首先從task queue中選擇最早進入的task執行,每執行完一個task都會檢查microtask queue是否爲空,若不爲空則執行完microtsk queue中的全部任務。而後再選擇task queue中最早進入的task執行,以此循環。
總結上述步驟爲流程圖:
console.log('這是開始');
setTimeout(function cb() {
console.log('這是來自第一個回調的消息');
}, 100);
console.log('這是一條消息');
setTimeout(function cb1() {
console.log('這是來自第二個回調的消息');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('這是結束');
複製代碼
輸出結果爲:
這是開始
這是一條消息
這是結束
promise1
promise2
這是來自第二個回調的消息
這是來自第一個回調的消息
複製代碼
輸出:
這是開始
這是一條消息
這是結束
複製代碼
進入microtask檢查點;從microtask queue中拿出promise1 then執行;將promise2 then推入microtask queue;執行完成後從microtask queue中刪除promise1 then任務;
輸出:
這是開始
這是一條消息
這是結束
promise1
複製代碼
查看microtask queue中是否還有任務;有則從microtask queue中拿出promise2 then執行;執行完成後從microtask queue中刪除promise2 then任務;
輸出:
這是開始
這是一條消息
這是結束
promise1
promise2
複製代碼
輸出:
這是開始
這是一條消息
這是結束
promise1
promise2
這是來自第二個回調的消息
複製代碼
檢查microtask檢查點,microtask queue爲空,繼續取出task queue中的任務setTimeout1 callback執行,執行完成後從task queue中刪除setTimeout1 callback任務;
輸出:
這是開始
這是一條消息
這是結束
promise1
promise2
這是來自第二個回調的消息
這是來自第一個回調的消息
複製代碼
console.log('script start')
async function async1() {
await async2();
console.log('async1 end');
setTimeout(function() {
console.log('async1 setTimeout')
}, 0);
}
async function async2() {
console.log('async2 end');
setTimeout(function() {
console.log('async2 setTimeout')
}, 0);
}
async1();
setTimeout(function() {
Promise.resolve().then(function() {
console.log('setTimeout promise');
})
console.log('setTimeout');
}, 0);
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
複製代碼
輸出結果爲:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
async2 setTimeout
setTimeout
setTimeout promise
async1 setTimeout
複製代碼
async函數是promise的一個語法糖,簡單理解爲:await中的語句至關於在promise.resolve()中;await後面的語句至關於.then中的語句
輸出:
script start
複製代碼
-- 2.1 執行到調用async1()語句,在async1中執行await async2,async2中的語句直接執行,async2 setTimeout callback被推入task queue中;async1中的await async2後面的語句至關於promise then被推入microtask queue中; - task queue:async2 setTimeout callback; - microtask queue:async1中的await async2後面的語句;
輸出:
script start
async2 end
複製代碼
-- 2.2. 執行到setTimeout將setTimeout callback 推入 task queue中; - task queue:async2 setTimeout callback、setTimeout callback; - microtask queue:async1中的await async2後面的語句; -- 2.3. 執行到promise,執行resolve,將promise then1推入microtask queue; - task queue:async2 setTimeout callback、setTimeout callback; - microtask queue:async1中的await async2後面的語句、promise then1;
輸出:
script start
async2 end
Promise
複製代碼
-- 2.4. 執行輸出script end;
輸出:
script start
async2 end
Promise
script end
複製代碼
【microtask 階段】執行async1中的await async2後面的語句,輸出內容,並將async1 setTimeout callback推入task queue中
輸出:
script start
async2 end
Promise
script end
async1 end
複製代碼
輸出:
script start
async2 end
Promise
script end
async1 end
promise1
複製代碼
輸出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
複製代碼
輸出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
async2 setTimeout
複製代碼
輸出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
async2 setTimeout
setTimeout
複製代碼
輸出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
async2 setTimeout
setTimeout
setTimeout promise
複製代碼
輸出:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
async2 setTimeout
setTimeout
setTimeout promise
async1 setTimeout
複製代碼