JS異步詳解 - 瀏覽器/Node/事件循環/消息隊列/宏任務/微任務

js異步歷史

一個 JavaScript 引擎會常駐於內存中,它等待着咱們把JavaScript 代碼或者函數傳遞給它執行javascript

在 ES3 和更早的版本中,JavaScript 自己尚未異步執行代碼的能力,引擎就把代碼直接順次執行了,異步任務都是宿主環境(瀏覽器)發起的(setTimeout、AJAX等)。html

在 ES5 以後,JavaScript 引入了 Promise,這樣,不須要瀏覽器的安排,JavaScript 引擎自己也能夠發起任務了java

JS異步實現原理

js爲單線程,js引擎中負責解析執行js代碼的線程只有一個(主線程),即每次只能作一件事,其餘IO操做放入任務隊列等待執行,異步過程當中,工做線程異步操做完成後須要通知主線程。那麼這個通知機制是利用消息隊列事件循環(EventLoop)實際上,主線程只會作一件事情,就是從消息隊列裏面取消息、執行消息,再取消息、再執行。當消息隊列爲空時,就會等待直到消息隊列變成非空。並且主線程只有在將當前的消息執行完成後,纔會去取下一個消息
node:node.js單線程只是一個js主線程,本質上的異步操做仍是由線程池完成的,node將全部的阻塞操做都交給了內部的線程池去實現,自己只負責不斷的往返調度,並無進行真正的I/O操做,從而實現異步非阻塞I/O,這即是node單線程的精髓之處了。node

瀏覽器

概念

  • 消息隊列:消息隊列是一個先進先出的隊列,它裏面存放着各類消息。
  • 事件循環:事件循環是指主線程重複從消息隊列中取消息、執行的過程。(瀏覽器至少有一個事件循環,一個事件循環至少有一個任務隊列(macrotask))
  • 微任務:git

    • JavaScript 引擎發起的任務 - JS 引擎級別
    • promise回調,MutationObserver,process.nextTick,Object.observe
  • 宏任務github

    • 宿主發起的任務,每次的一段js代碼執行過程,其實都是一個宏觀任務 - 宿主級別
    • 總體的js代碼,事件回調,XHR回調,定時器(setTimeout/setInterval/setImmediate),IO操做,UI render
  • 宏任務和微任務關係:每一個macro宏任務會維護一個micro微任務列表

事件循環過程

  • 首先咱們分析有多少個宏任務;
  • 在每一個宏任務中,分析有多少個微任務;
  • 根據調用次序,肯定宏任務中的微任務執行次序;
  • 根據宏任務的觸發規則和調用次序,肯定宏任務的執行次序;
  • 肯定整個順序

clipboard.png

視圖渲染時機:

  • 本輪事件循環的microtask隊列被執行完以後(不是每輪事件循環都會執行視圖更新,瀏覽器有本身的優化策略)
  • 注意:執行任務的耗時會影響視圖渲染的時機。一般瀏覽器以每秒60幀(60fps)的速率刷新頁面(16.7ms渲染一幀)因此若是要讓用戶以爲順暢,單個macrotask及它相關的全部microtask最好能在16.7ms內完成。

clipboard.png

Node

概念

  • 非阻塞 I/O 操做:儘管 JavaScript 是單線程處理的——當有可能的時候,它們會把操做轉移到系統內核中去,當其中的一個操做完成的時候,內核通知 Node.js 將適合的回調函數添加到 輪詢 隊列中等待時機執行

事件循環過程

  • 過程promise

    • event loop 的每一個階段都有一個任務隊列(一個 FIFO 隊列來執行回調)
    • 當 event loop 到達某個階段時,將執行該階段的任務隊列,直到隊列清空或執行的回調達到系統上限後,纔會轉入下一個階段
    • 當全部階段被順序執行一次後,稱 event loop 完成了一個 tick
  • 每次事件循環都包含了6個階段瀏覽器

    • timers 階段:這個階段執行timer(setTimeoutsetInterval)的回調
    • I/O callbacks 階段:執行一些系統調用錯誤,好比網絡通訊的錯誤回調
    • idle, prepare 階段:僅node內部使用
    • poll 階段:獲取新的I/O事件, 適當的條件下node將阻塞在這裏
    • check 階段:執行 setImmediate() 的回調
    • close callbacks 階段:執行 socketclose 事件回調

clipboard.png

  • timers 階段

    Node 會去檢查有無已過時的timer,若是有則把它的回調壓入timer的任務隊列中等待執行網絡

    技術上來講,poll 階段控制 timers 何時執行。異步

  • poll 階段

    • poll 階段主要有2個功能:

      • 處理 poll 隊列的事件
      • 當有已超時的 timer,執行它的回調函數
    • 執行過程:當event loop進入 poll 階段,而且 沒有設定的timers(there are no timers scheduled),會發生下面兩件事之一:

      若是 poll 隊列不空,event loop會遍歷隊列並同步執行回調,直到隊列清空或執行的回調數到達系統上限;

      若是 poll 隊列爲空,則發生如下兩件事之一:

      1. 若是代碼已經被setImmediate()設定了回調, event loop將結束 poll 階段進入 check 階段來執行 check 隊列(裏的回調)。
      2. 若是代碼沒有被setImmediate()設定回調,event loop將阻塞在該階段等待回調被加入 poll 隊列,並當即執行。
    • 當event loop進入 poll 階段,而且 有設定的timers,一旦 poll 隊列爲空(poll 階段空閒狀態): event loop將檢查timers,若是有1個或多個timers的下限時間已經到達,event loop將繞回 timers 階段,並執行 timer隊列。
    • 注意:沒有setImmediate()會致使event loop阻塞在poll階段,這樣以前設置的timer豈不是執行不了了?因此咧,在poll階段event loop會有一個檢查機制,檢查timer隊列是否爲空,若是timer隊列非空,event loop就開始下一輪事件循環,即從新進入到timer階段。

process.nextTick() VS setImmediate()

  • process.nextTick()

    • 在各個事件階段之間執行,一旦執行,要直到nextTick隊列被清空,纔會進入到下一個事件階段
    • 遞歸調用 process.nextTick(),會致使出現I/O starving(飢餓)
  • setImmediate

對比

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

//瀏覽器:
timer1
promise1
timer2
promise2

// node
timer1
timer2
promise1
promise2

http://lynnelv.github.io/img/...

clipboard.png

http://lynnelv.github.io/img/...

clipboard.png

補充閱讀

  • node單線程底層實現機制:

https://juejin.im/post/5b61d8...

https://yq.aliyun.com/article...

https://juejin.im/post/5b1e55...

  • node setTimeOut(), setInterval(), setImmediate() 以及 process.nextTick()區別
  • js 三種定時器的區別

https://www.cnblogs.com/onepi...

相關文章
相關標籤/搜索