這段程式碼到底怎麼走?終於搞定Event loop

rocYoung發表於2018-03-27

眾所周知,js是一門單執行緒的程式語言,在設計之初,它就註定了單執行緒的命運,比如當我們處理dom時,如果有多個執行緒同時操作一個dom,那將非常混亂。

既然是單執行緒,那麼它一定有一套嚴謹的規則,來使程式碼能夠乖乖的按開發者的設計執行,今天我們就來研究其中的奧祕,瞭解一下js的event loop(事件迴圈)。

同步/非同步

聊js事件環,繞不開聊非同步(在我的另一篇文章擁抱並扒光Promise中對Promise這種非同步解決方案有詳細介紹)

為什麼要非同步?假設沒有非同步,我們傳送一個ajax請求,後端程式碼執行的很慢,這時瀏覽器會發生阻塞,如果十秒才響應,這十秒我們該幹嘛?(或許可以看博爾特跑個百米)

雖然在網頁誕生之初,確實有這樣的情況,但如今這樣的頁面是會被使用者罵孃的。於是非同步的作用顯露無遺,js開啟一個非同步執行緒,什麼時候請求完成,什麼時候執行回撥函式,而這期間,其他程式碼也可以正常執行。

任務佇列(task queue)

既然是單執行緒,就像一次只能過一個人的獨木橋,人要排隊,那麼程式碼也要排隊。這時,同步程式碼和非同步程式碼的排隊機制是不一樣的

同步:在主執行緒(相當於獨木橋上)上排隊的任務,前一個任務執行完,下一個任務才可以執行,如果前一個任務沒執行完,下一個任務要一直等待。就像過獨木橋,前面的人不過去,你死等也得等,不然就5253B翻騰兩週半入水。

非同步:主執行緒先不管IO裝置,掛起處於等待中的任務,先執行排在後面的任務。等到IO裝置返回了結果,再回過頭,把掛起的任務繼續執行下去。就像過獨木橋,你害怕不敢過,你就讓後面的人先過,什麼時候你敢了你再過。而你調整心態的過程,主執行緒不考慮。

  • 同步任務在主執行緒上執行,形成一個執行棧(xecution context stack)
  • 主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
  • 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。

主執行緒會不斷的重複以上三步,這樣就構成了事件環,用圖表示

這段程式碼到底怎麼走?終於搞定Event loop

瀏覽器中的Event Loop

  • 堆(heap)在JS執行時用來存放物件。
  • 棧(stack)遵循“先進後出”原則,我們知道棧可以存放物件的地址,但本文中的棧是指用來執存放行JS主執行緒的執行棧(execution context stack)。

這段程式碼到底怎麼走?終於搞定Event loop

通過這張圖,我們可以知道,主執行緒執行時,產生堆和執行棧,棧中的程式碼會呼叫一些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

這段程式碼到底怎麼走?終於搞定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


參考:

圖解搞懂JavaScript引擎Event Loop

JavaScript 執行機制詳解:再談Event Loop

相關文章