眾所周知,js是一門單執行緒的程式語言,在設計之初,它就註定了單執行緒的命運,比如當我們處理dom時,如果有多個執行緒同時操作一個dom,那將非常混亂。
既然是單執行緒,那麼它一定有一套嚴謹的規則,來使程式碼能夠乖乖的按開發者的設計執行,今天我們就來研究其中的奧祕,瞭解一下js的event loop(事件迴圈)。
同步/非同步
聊js事件環,繞不開聊非同步(在我的另一篇文章擁抱並扒光Promise中對Promise這種非同步解決方案有詳細介紹)
為什麼要非同步?假設沒有非同步,我們傳送一個ajax請求,後端程式碼執行的很慢,這時瀏覽器會發生阻塞,如果十秒才響應,這十秒我們該幹嘛?(或許可以看博爾特跑個百米)
雖然在網頁誕生之初,確實有這樣的情況,但如今這樣的頁面是會被使用者罵孃的。於是非同步的作用顯露無遺,js開啟一個非同步執行緒,什麼時候請求完成,什麼時候執行回撥函式,而這期間,其他程式碼也可以正常執行。
任務佇列(task queue)
既然是單執行緒,就像一次只能過一個人的獨木橋,人要排隊,那麼程式碼也要排隊。這時,同步程式碼和非同步程式碼的排隊機制是不一樣的
同步:在主執行緒(相當於獨木橋上)上排隊的任務,前一個任務執行完,下一個任務才可以執行,如果前一個任務沒執行完,下一個任務要一直等待。就像過獨木橋,前面的人不過去,你死等也得等,不然就5253B翻騰兩週半入水。
非同步:主執行緒先不管IO裝置,掛起處於等待中的任務,先執行排在後面的任務。等到IO裝置返回了結果,再回過頭,把掛起的任務繼續執行下去。就像過獨木橋,你害怕不敢過,你就讓後面的人先過,什麼時候你敢了你再過。而你調整心態的過程,主執行緒不考慮。
- 同步任務在主執行緒上執行,形成一個執行棧(xecution context stack)
- 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
- 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
主執行緒會不斷的重複以上三步,這樣就構成了事件環,用圖表示
瀏覽器中的Event Loop
- 堆(heap)在JS執行時用來存放物件。
- 棧(stack)遵循“先進後出”原則,我們知道棧可以存放物件的地址,但本文中的棧是指用來執存放行JS主執行緒的執行棧(execution context stack)。
通過這張圖,我們可以知道,主執行緒執行時,產生堆和執行棧,棧中的程式碼會呼叫一些api,比如seTtimeou、click等,這些非同步操作會講他們的回撥放入callback queue中,當執行棧中的程式碼執行完,主執行緒回去讀取queue中的任務。
console.log(1)
setTimeout(function(){
console.log(2)
})
console.log(3)
複製程式碼
我們都知道結果是1 3 2,結合上面我們梳理一下這段程式碼的執行順序
1、從上到下執行執行棧中的同步程式碼console.log(1)
2、看到setTimeout,把回撥函式放入任務佇列中去
3、執行console.log(3)
4、主執行緒上沒有任務了,去任務佇列中執行setTimeout的回撥,console.log(2)
Node中的Event Loop
顯然node要比瀏覽器複雜一些,它的流程是這樣的:
- V8引擎解析JavaScript指令碼。
- 解析後的程式碼,呼叫Node API。
- libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎。
- V8引擎再將結果返回給使用者。
Node還有一些不同,它提供了另外兩個與"任務佇列"有關的方法:process.nextTick和setImmediate。它們可以幫助我們加深對"任務佇列"的理解。
process.nextTick方法可以在當前"執行棧"的尾部,下一次Event Loop(主執行緒讀取"任務佇列")之前,觸發回撥函式。也就是說,它指定的任務總是發生在所有非同步任務之前。
setImmediate方法則是在當前"任務佇列"的尾部新增事件,也就是說,它指定的任務總是在下一次Event Loop時執行,這與setTimeout(fn, 0)很像。
大概可以理解成process.nextTick有權插隊
setTimeout(function(){
console.log(1)
})
process.nextTick(function () {
console.log(2);
process.nextTick(function (){
console.log(3)
});
});
setTimeout(function () {
console.log(4);
})
複製程式碼
雖然1在上面,但結果是2 3 1 4,就像我們上面說的,process.nextTick會在主執行緒讀取任務佇列時插隊
再看setImmediate
setImmediate(function () {
console.log(1);
setImmediate(function B(){
console.log(2)
})
})
setTimeout(function () {
console.log(3);
}, 0)
複製程式碼
結果可能是312,也可能是132
微任務/巨集任務
為什麼會出現上面有的先有的後的情況呢,難道除了人類社會程式碼世界也有特權麼,是的,我們將任務分為兩種:
微任務Microtask,有特權,可以插隊,包括原生Promise,Object.observe(已廢棄), MutationObserver, MessageChannel;
巨集任務Macrotask,沒有特權,包括setTimeout, setInterval, setImmediate, I/O;
最後,一段比較複雜的程式碼收尾。
console.log("1");
setTimeout(()=>{
console.log(2)
Promise.resolve().then(()=>{
console.log(3);
process.nextTick(function foo() {
console.log(4);
});
})
})
Promise.resolve().then(()=>{
console.log(5);
setTimeout(()=>{
console.log(6)
})
Promise.resolve().then(()=>{
console.log(7);
})
})
process.nextTick(function foo() {
console.log(8);
process.nextTick(function foo() {
console.log(9);
});
});
console.log("10")
複製程式碼
執行順序:
1,輸出1
2,將setTimeout(2)push進巨集任務
3,將then(5)push進微任務
4,在執行棧底部新增nextTick(8)
5,輸出10
6,執行nextTick(8)
7,輸出8
8,在執行棧底部新增nextTick(9)
9,輸出9
10,執行微任務then(5)
11,輸出5
12,將setTimeout(6)push進巨集任務
13,將then(7)push進微任務
14,執行微任務then(7)
15,輸出7
16,取出setTimeout(2)
17,輸出2
18,將then(3)push進微任務
19,執行微任務then(3)
20,輸出3
21,在執行棧底部新增nextTick(4)
22,輸出4
23,取出setTimeout(6)
24,輸出6