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

衆所周知,js是一門單線程的編程語言,在設計之初,它就註定了單線程的命運,好比當咱們處理dom時,若是有多個線程同時操做一個dom,那將很是混亂。html

既然是單線程,那麼它必定有一套嚴謹的規則,來使代碼可以乖乖的按開發者的設計運行,今天咱們就來研究其中的奧祕,瞭解一下js的event loop(事件循環)。node

同步/異步

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

爲何要異步?假設沒有異步,咱們發送一個ajax請求,後端代碼運行的很慢,這時瀏覽器會發生阻塞,若是十秒才響應,這十秒咱們該幹嗎?(或許能夠看博爾特跑個百米)編程

雖然在網頁誕生之初,確實有這樣的狀況,但現在這樣的頁面是會被用戶罵孃的。因而異步的做用顯露無遺,js開啓一個異步線程,何時請求完成,何時執行回調函數,而這期間,其餘代碼也能夠正常運行。後端

任務隊列(task queue)

既然是單線程,就像一次只能過一我的的獨木橋,人要排隊,那麼代碼也要排隊。這時,同步代碼和異步代碼的排隊機制是不同的api

同步:在主線程(至關於獨木橋上)上排隊的任務,前一個任務執行完,下一個任務才能夠執行,若是前一個任務沒執行完,下一個任務要一直等待。就像過獨木橋,前面的人不過去,你死等也得等,否則就5253B翻騰兩週半入水。瀏覽器

異步:主線程先無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。就像過獨木橋,你懼怕不敢過,你就讓後面的人先過,何時你敢了你再過。而你調整心態的過程,主線程不考慮。dom

  • 同步任務在主線程上執行,造成一個執行棧(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,結合上面咱們梳理一下這段代碼的執行順序

一、從上到下運行執行棧中的同步代碼console.log(1)

二、看到setTimeout,把回調函數放入任務隊列中去

三、執行console.log(3)

四、主線程上沒有任務了,去任務隊列中執行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


參考:

圖解搞懂JavaScript引擎Event Loop

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

相關文章
相關標籤/搜索