前言
眾所周知,javascript是一門單執行緒語言,而當我們使用ajax和服務端進行通訊的時候是需要一定時間的,這樣當前執行緒就會被阻塞,使瀏覽器失去相應。因此,當js執行執行一些長時間的任務時,我們希望有一種非同步的方式處理這種任務。事件迴圈(event loop)就是如何處理非同步執行順序的一種機制。
$.get(url, function (data) {
//do something
});
複製程式碼
瀏覽器中的事件迴圈
接下來會一一介紹,事件迴圈中的執行棧
、事件佇列
、巨集任務
、微任務
等概念
什麼是執行棧
執行棧就是js程式碼執行的地方,上圖call stack所示。當下面程式執行時,會推送的呼叫棧中被執行。
console.log(`Hi`);
setTimeout(function cb1() {
console.log(`cb1`);
}, 500);
console.log(`Bye`);
複製程式碼
什麼是事件佇列
當瀏覽器中的事件監聽函式被觸發(DOM)、網路請求的相應(ajax)、定時器被觸發(setTimeout)相對應的回撥函式就會被推送到事件佇列中,等待執行;如上圖中的Callback Queue。
什麼是事件迴圈
事件迴圈是一個這樣的過程:當執行棧中的任務結束之後,會將事件佇列中的第一個任務推入到執行棧中執行,當任務處理完畢,又會取事件佇列中的第一個任務,如此往復,便構成了事件迴圈。
對應到下面程式碼中。
console.log(`Hi`);
setTimeout(function cb1() {
console.log(`cb1`);
}, 500);
console.log(`Bye`);
複製程式碼
- 程式推送到執行棧中被執行
- 執行console語句、輸出Hi
- 執行setTimeou語句
- 執行console語句、輸出Bye
- 500ms的時候,setTimeout的回撥函式被推送到事件佇列中
- 此時事件佇列中只有setTimeout的回撥函式這一個任務,會被推到執行棧中執行
- console語句執行、輸出cb1
通過上面的例子會對執行棧和事件佇列有個基本的認識。由於JS是單執行緒的,同步任務會造成瀏覽器阻塞,我們把任務分成一個一個的非同步任務,通過事件迴圈來執行事件佇列中的任務。這就使得當我們掛起某一個任務的時候可以去做一些其他的事情,而不需要等待這個任務執行完畢。所以事件迴圈的執行機制大致分為以下步驟:
1、檢查事件佇列是否為空,如果為空,則繼續檢查;如不為空,則執行 2;
2、取出事件佇列的首部,壓入執行棧;
3、執行任務;
4、檢查執行棧,如果執行棧為空,則跳回第 1 步;如不為空,則繼續檢查;
瀏覽器渲染時機
我們知道DOM操作會觸發瀏覽器渲染,如增、刪節點,改變背景顏色。那麼這類操作是如何在瀏覽器當中奏效的?
至此我們已經知道了事件迴圈是如何執行的,事件迴圈器會不停的檢查事件佇列,如果不為空,則取出隊首壓入執行棧執行。當一個任務執行完畢之後,事件迴圈器又會繼續不停的檢查事件佇列,不過在這間,瀏覽器會對頁面進行渲染。這就保證了使用者在瀏覽頁面的時候不會出現頁面阻塞的情況,這也使 JS 動畫成為可能。
function move() {
setTimeout(() => {
dom.style.left = dom.offsetLeft + 10 + `px`
move()
}, 15);
}
move()
複製程式碼
現在用事件迴圈的機制說明js動畫的過程。上面程式碼會在執行棧中執行,move函式被呼叫,setTimeout的回撥函式15ms之後會被推送到事件佇列中。此時執行棧中的任務結束,瀏覽器渲染、檢查事件佇列不斷迴圈。當15ms之後事件佇列中有任務時,會被推送到執行棧中執行,這時dom節點向右偏移10px,move函式執行、執行棧結束,瀏覽渲染、檢查事件佇列。如此往復就形成了動畫。
巨集任務和微任務(microtask)
先看一段程式碼,是如何輸出的;
console.log(`script start`);
setTimeout(function () {
console.log(`setTimeout`);
}, 0);
Promise.resolve().then(function () {
console.log(`promise1`);
}).then(function () {
console.log(`promise2`);
});
console.log(`script end`);
複製程式碼
答案是:`script start`
、`script end`
、`promise1`
、`promise2`
、`setTimeout`
。
setTimeout的回撥函式是巨集任務、Promise的回撥函式是微任務。微任務和巨集任務一樣遵循事件迴圈機制,但是他們還是有些差別。
1、巨集任務和微任務的事件佇列是相互獨立的;
2、微任務佇列的檢查時機早於巨集任務。(執行棧中任務結束就會馬上清空微任務事件佇列)
根據上面的規則,解釋程式碼的輸出。
-
執行棧中的程式碼執行,巨集任務推入巨集任務事件佇列、微任務推入微任務事件佇列,執行棧任務結束
-
檢查微任務事件佇列,此時已經有Promise的回撥函式,推入執行棧,輸出
promise1
。Promise還有回撥函式,推入微任務事件佇列,執行棧結束。 -
檢查微任務事件佇列,推入執行棧,輸出
promise2
,執行棧結束。 -
檢查微任務事件佇列,此時被清空
-
檢查巨集任務事件佇列,推入執行棧,輸出
setTimeout
,執行棧結束。巨集任務有: **setTimeout** 、**setImmediate** 、 **MessageChannel** 微任務有: **setTimeout** 、**setImmediate** 、 **MessageChannel** 複製程式碼
Node.js中的事件迴圈
Node中的事件迴圈是和瀏覽器有很大區別的
當Node.js啟動時,會初始化event loop;每個event loop都會包含按如下順序六個迴圈階段
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────
複製程式碼
- timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預定的callback;
- I/O callbacks 階段: 執行除了 close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks之外的callbacks;
- idle, prepare 階段: 僅node內部使用;
- poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裡;
- check 階段: 執行setImmediate() 設定的callbacks;
- close callbacks 階段: 比如socket.on(‘close’, callback)的callback會在這個階段執行。
每一個階段都有一個裝有callbacks的fifo queue(佇列),當event loop執行到一個指定階段時,
node將執行該階段的fifo queue(佇列),當佇列callback執行完或者執行callbacks數量超過該階段的上限時,event loop會轉入下一下階段。
Node.js中的巨集任務和微任務
巨集任務:setTimeout和setImmediate
複製程式碼
- setTimeout 設計在poll階段為空閒時,且設定時間到達後執行;但其在timer階段執行
- setImmediate 設計在check階段執行;
誰先輸出,誰後輸出?
setTimeout(function timeout () {
console.log(`timeout`);
},0);
setImmediate(function immediate () {
console.log(`immediate`);
});
複製程式碼
答案是不確定的。有兩個前提我們是需要清楚的;
- event loop初始化是需要一定時間的
- setTimeout有最小毫秒數的,通常是4ms。
當:event loop準備時間 > setTimeout最小毫秒數。從timers階段檢查,此時佇列中已經有setTimeout的任務,所以timeout
先輸出;
當:event loop準備時間 < setTimeout最小毫秒數。從timers階段檢查,此時佇列是空的就下檢查接下來的階段,到check階段,已經有setImmediate的任務,所以immediate
先輸出;
微任務:process.nextTick()和Promise.then()
複製程式碼
微任務不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行;nextTick比Promise.then()先執行
下面程式碼是如何執行的。
setImmediate(() => {
console.log(`setImmediate1`)
setTimeout(() => {
console.log(`setTimeout1`)
}, 0);
})
setTimeout(()=>{
process.nextTick(()=>console.log(`nextTick`))
console.log(`setTimeout2`)
setImmediate(()=>{
console.log(`setImmediate2`)
})
},0);
複製程式碼
- 從前面的知識知道,此時setTimeout和setImmediate執行順序是不確定的。
- 假設setImmediate先執行,輸出
setImmediate1
,setTimeout的任務新增到timer階段 - 檢查timer階段,這時已經有兩個任務。先執行之前的第一個任務,nextTick新增到微任務佇列,輸出
setTimeout2
,setImmediate的任務新增到check階段。 - timer中還有一個任務,執行輸出
setTimeout1
- 切換階段,微任務執行,輸出
nextTick
- 檢查check階段,輸出
setImmediate2
思考題
let fs = require(`fs`)
fs.readFile(`./1.txt`, `utf8`, function (err, data) {
setTimeout(() => {
console.log(`setTimeout`)
}, 0);
setImmediate(() => {
console.log(`setImmediate`)
})
})
複製程式碼
這種情況下的setTimeout和setImmediate執行的順序確定嗎?readFile的回撥函式是在poll階段執行
答案是setImmediate
比setTimeout
先執行
結語
瀏覽器中和Node.js中的事件迴圈可以說是兩套不同的機制,做個總結,希望有所幫助。