前端面試查漏補缺--(十五) Event Loop

前言

本系列最開始是爲了本身面試準備的.後來發現整理愈來愈多,差很少有十二萬字符,最後決定仍是分享出來給你們.javascript

爲了分享整理出來,花費了本身大量的時間,起碼是隻本身用的三倍時間.若是喜歡的話,歡迎收藏,關注我!謝謝!html

文章連接

合集篇:

前端面試查漏補缺--Index篇(12萬字符合集) 包含目前已寫好的系列其餘十幾篇文章.後續新增值文章不會再在每篇添加連接,強烈建議議點贊,關注合集篇!!!!,謝謝!~前端

後續更新計劃

後續還會繼續添加設計模式,前端工程化,項目流程,部署,閉環,vue常考知識點 等內容.若是以爲內容不錯的話歡迎收藏,關注我!謝謝!vue

求一分內推

目前本人也在準備跳槽,但願各位大佬和HR小姐姐能夠內推一份靠譜的武漢 前端崗位!郵箱:bupabuku@foxmail.com.謝謝啦!~java

Event loop的初步理解

相信你們若是對Event loop有必定了解的話,大概都會知道它的大概步驟是:面試

  • 1,Javascript的事件分爲同步任務和異步任務.
  • 2,遇到同步任務就放在執行棧中執行.
  • 3,遇到異步任務就放到任務隊列之中,等到執行棧執行完畢以後再去執行任務隊列之中的事件.

上面的步驟說得沒錯,很對,可是太淺了.如今的面試應該不會這麼簡單,起碼得加上宏任務,微任務.再整上幾個Promise,async await,讓你判斷.不然都很差意思叫面試題!ajax

爲了不被面試官吊起來打的狀況,咱們如今來詳細地理一理Event Loop.算法

Event loop 相關概念

JS調用棧

Javascript 有一個 主線程(main thread)和 調用棧(call-stack),全部的代碼都要經過函數,放到調用棧(也被稱爲執行棧)中的任務等待主線程執行。設計模式

JS調用棧採用的是後進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。前端工程化

WebAPIs

MDN的解釋: Web 提供了各類各樣的 API 來完成各類的任務。這些 API 能夠用 JavaScript 來訪問,令你能夠作不少事兒,小到對任意 window 或者 element作小幅調整,大到使用諸如 WebGL 和 Web Audio 的 API 來生成複雜的圖形和音效。

Web API 接口參考

總結: 就是瀏覽器提供一些接口,讓JavaScript能夠調用,這樣就能夠把任務甩給瀏覽器了,這樣就能夠實現異步了!

任務隊列(Task Queue)

"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,若是存在"定時器",主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。

同步任務和異步任務

Javascript單線程任務被分爲同步任務和異步任務.

  • 同步任務會在調用棧 中按照順序等待主線程依次執行.
  • 異步任務會甩給在WebAPIs處理,處理完後有告終果後,將註冊的回調函數放入任務隊列中等待主線程空閒的時候(調用棧被清空),被讀取到棧內等待主線程的執行。

宏任務(MacroTask)和 微任務(MicroTask)

JavaScript中,任務被分爲兩種,一種宏任務(MacroTask)也叫Task,一種叫微任務(MicroTask)。

宏任務(MacroTask)

  • script(總體代碼)setTimeoutsetIntervalsetImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)、I/OUI Rendering

微任務(MicroTask)

  • Process.nextTick(Node獨有)PromiseObject.observe(廢棄)MutationObserver(具體使用方式查看這裏

Event loop 執行過程

首先宏觀上是按照這樣的順序執行.也就是前面在"Event loop的初步理解"裏講到的過程

注意:

  • 只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重複。
  • 在上圖的Event Table裏存放着宏任務與微任務,因此在它裏面 還發生了一些更細緻的事情.

前面介紹宏任務的時候,提過script也屬於其中.那麼一段代碼塊就是一個宏任務。故全部通常執行代碼塊的時候,先執行的是宏任務script,也就是程序執行進入主線程了,主線程再會根據不一樣的代碼再分微任務和宏任務等待主線程執行完成後,不停地循環執行。

主線程(宏任務) => 微任務 => 宏任務 => 主線程

事件循環的順序是從script開始第一次循環,隨後全局上下文進入函數調用棧,碰到macro-task就將其交給處理它的模塊處理完以後將回調函數放進macro-task的隊列之中,碰到micro-task也是將其回調函數放進micro-task的隊列之中。直到函數調用棧清空只剩全局執行上下文,而後開始執行全部的micro-task。當全部可執行的micro-task執行完畢以後。 接着瀏覽器會執行下必要的渲染 UI,而後循環再次執行macro-task中的一個任務隊列,執行完以後再執行全部的micro-task,就這樣一直循環。

注意: 經過上述的 Event loop 順序可知,若是宏任務中的異步代碼有大量的計算而且須要操做 DOM 的話,爲了更快的 界面響應,咱們能夠把操做 DOM 放入微任務中。

例子分析

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')

複製代碼

這裏須要先理解async/await

async/await 在底層轉換成了 promisethen 回調函數。 也就是說,這是 promise 的語法糖。

每次咱們使用 await, 解釋器都建立一個 promise 對象,而後把剩下的 async 函數中的操做放到 then 回調函數中。

async/await 的實現,離不開 Promise。從字面意思來理解,async 是「異步」的簡寫,而 awaitasync wait 的簡寫能夠認爲是等待異步方法執行完成。

關於73如下版本和73版本的區別

  • 在73版本如下,先執行promise1promise2,再執行async1
  • 在73版本,先執行async1再執行promise1promise2

主要緣由是由於在谷歌(金絲雀)73版本中更改了規範,以下圖所示:

  • 區別在於RESOLVE(thenable)和之間的區別Promise.resolve(thenable)

在73如下版本中

  • 首先,傳遞給 await 的值被包裹在一個 Promise 中。而後,處理程序附加到這個包裝的 Promise,以便在 Promise 變爲 fulfilled 後恢復該函數,而且暫停執行異步函數,一旦 promise 變爲 fulfilled,恢復異步函數的執行。
  • 每一個 await 引擎必須建立兩個額外的 Promise(即便右側已是一個 Promise)而且它須要至少三個 microtask 隊列 tickstick爲系統的相對時間單位,也被稱爲系統的時基,來源於定時器的週期性中斷(輸出脈衝),一次中斷表示一個tick,也被稱作一個「時鐘滴答」、時標。)。

引用賀老師知乎上的一個例子

async function f() {
  await p
  console.log('ok')
}
複製代碼

簡化理解爲:

function f() {
  return RESOLVE(p).then(() => {
    console.log('ok')
  })
}

複製代碼
  • 若是 RESOLVE(p) 對於 ppromise 直接返回 p 的話,那麼 pthen 方法就會被立刻調用,其回調就當即進入 job 隊列。
  • 而若是 RESOLVE(p) 嚴格按照標準,應該是產生一個新的 promise,儘管該 promise肯定會 resolvep,但這個過程自己是異步的,也就是如今進入 job 隊列的是新 promiseresolve過程,因此該 promisethen 不會被當即調用,而要等到當前 job 隊列執行到前述 resolve 過程纔會被調用,而後其回調(也就是繼續 await 以後的語句)才加入 job 隊列,因此時序上就晚了。

谷歌(金絲雀)73版本中

  • 使用對PromiseResolve的調用來更改await的語義,以減小在公共awaitPromise狀況下的轉換次數。
  • 若是傳遞給 await 的值已是一個 Promise,那麼這種優化避免了再次建立 Promise 包裝器,在這種狀況下,咱們從最少三個 microtick 到只有一個 microtick

詳細過程:

73如下版本

  • 首先,打印script start,調用async1()時,返回一個Promise,因此打印出來async2 end
  • 每一個 await,會新產生一個promise,但這個過程自己是異步的,因此該await後面不會當即調用。
  • 繼續執行同步代碼,打印Promisescript end,將then函數放入微任務隊列中等待執行。
  • 同步執行完成以後,檢查微任務隊列是否爲null,而後按照先入先出規則,依次執行。
  • 而後先執行打印promise1,此時then的回調函數返回undefinde,此時又有then的鏈式調用,又放入微任務隊列中,再次打印promise2
  • 再回到await的位置執行返回的 Promiseresolve 函數,這又會把 resolve 丟到微任務隊列中,打印async1 end
  • 微任務隊列爲空時,執行宏任務,打印setTimeout

谷歌(金絲雀73版本)

  • 若是傳遞給 await 的值已是一個 Promise,那麼這種優化避免了再次建立 Promise 包裝器,在這種狀況下,咱們從最少三個 microtick 到只有一個 microtick
  • 引擎再也不須要爲 await 創造 throwaway Promise - 在絕大部分時間。
  • 如今 promise 指向了同一個 Promise,因此這個步驟什麼也不須要作。而後引擎繼續像之前同樣,建立 throwaway Promise,安排 PromiseReactionJobmicrotask 隊列的下一個 tick 上恢復異步函數,暫停執行該函數,而後返回給調用者。

具體詳情查看(這裏)。

Node.js的Event Loop

Node.js的運行機制以下。

(1)V8引擎解析JavaScript腳本。

(2)解析後的代碼,調用Node API。

(3)libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎。

(4)V8引擎再將結果返回給用戶。

Node 的 Event loop 分爲 6 個階段,它們會按照順序反覆運行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
複製代碼

NodeEvent loop一共分爲6個階段,每一個細節具體以下:

  • timers: 執行setTimeoutsetInterval中到期的callback
  • pending callback: 上一輪循環中少數的callback會放在這一階段執行。
  • idle, prepare: 僅在內部使用。
  • poll: 最重要的階段,執行pending callback,在適當的狀況下回阻塞在這個階段。
  • check: 執行setImmediate(setImmediate()是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成以後當即執行setImmediate指定的回調函數)的callback
  • close callbacks: 執行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)

關於Node.js的Event Loop更詳細的過程能夠參考這篇文章

補充: 線程和進程

在概念上

進程是應用程序的執行實例,每個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成;進程在運行過程當中可以申請建立和使用系統資源(如獨立的內存區域等),這些資源也會隨着進程的終止而被銷燬。

而線程則是進程內的一個獨立執行單元,在不一樣的線程之間是能夠共享進程資源的,因此在多線程的狀況下,須要特別注意對臨界資源的訪問控制。在系統建立進程以後就開始啓動執行進程的主線程,而進程的生命週期和這個主線程的生命週期一致,主線程的退出也就意味着進程的終止和銷燬。主線程是由系統進程所建立的,同時用戶也能夠自主建立其它線程,這一系列的線程都會併發地運行於同一個進程中。

比喻理解

一個進程比如是一個工廠,每一個工廠有它的獨立資源(類比到計算機上就是系統分配的一塊獨立內存),並且每一個工廠之間是相互獨立、沒法進行通訊。

每一個工廠都有若干個工人(一個工人便是一個線程,一個進程由一個或多個線程組成),多個工人能夠協做完成任務(即多個線程在進程中協做完成任務),固然每一個工人能夠共享此工廠的空間和資源(即同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等))。

到此你應該能初步理解了進程和線程之間的關係,這將有助於咱們理解瀏覽器爲何是多進程的,而JavaScript是單線程。

瀏覽器是多進程的(一個窗口就是一個進程),每一個進程包含多個線程.但JavaScript是單線程的.一個主線程(一個stack),多個子線程.

爲何JavaScript是單線程?

假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準? 因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。

瀏覽器中其餘的線程:

js既然是單線程,那麼確定是排隊執行代碼,那麼怎麼去排這個隊,就是Event Loop。雖然JS是單線程,但瀏覽器不是單線程。瀏覽器中分爲如下幾個線程:

  • js線程
  • UI線程
  • 事件線程(onclick,onchange,...)
  • 定時器線程(setTimeout, setInterval)
  • 異步http線程(ajax)

其中JS線程和UI線程相互互斥,也就是說,當UI線程在渲染的時候,JS線程會掛起,等待UI線程完成,再執行JS線程

爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

感謝及參考

相關文章
相關標籤/搜索