前言
本系列最開始是為了自己面試準備的.後來發現整理越來越多,差不多有十二萬字元,最後決定還是分享出來給大家.
為了分享整理出來,花費了自己大量的時間,起碼是隻自己用的三倍時間.如果喜歡的話,歡迎收藏,關注我!謝謝!
文章連結
- 前端面試查漏補缺--(一) 防抖和節流
- 前端面試查漏補缺--(二) 垃圾回收機制
- 前端面試查漏補缺--(三) 跨域及常見解決辦法
- 前端面試查漏補缺--(四) 前端本地儲存
- 前端面試查漏補缺--(五) 渲染機制及重繪和迴流
- 前端面試查漏補缺--(六) 瀏覽器快取
- 前端面試查漏補缺--(七) XSS攻擊與CSRF攻擊
- 前端面試查漏補缺--(八) 前端加密
- 前端面試查漏補缺--(九) HTTP與HTTPS
- 前端面試查漏補缺--(十) 前端鑑權
- 前端面試查漏補缺--(十一) 前端軟體架構模式MVC/MVP/MVVM
- 前端面試查漏補缺--(十二) 從輸入URL到看到頁面發生的全過程(含三握手,四揮手詳解)
- 前端面試查漏補缺--(十三) 記憶體洩漏
- 前端面試查漏補缺--(十四) 演算法及排序
- 前端面試查漏補缺--(十五) Event Loop
合集篇:
前端面試查漏補缺--Index篇(12萬字元合集) 包含目前已寫好的系列其他十幾篇文章.後續新增值文章不會再在每篇新增連結,強烈建議議點贊,關注合集篇!!!!,謝謝!~
後續更新計劃
後續還會繼續新增設計模式,前端工程化,專案流程,部署,閉環,vue常考知識點 等內容.如果覺得內容不錯的話歡迎收藏,關注我!謝謝!
求一份內推
目前本人也在準備跳槽,希望各位大佬和HR小姐姐可以內推一份靠譜的武漢 前端崗位!郵箱:bupabuku@foxmail.com.謝謝啦!~
Event loop的初步理解
相信大家如果對Event loop有一定了解的話,大概都會知道它的大概步驟是:
- 1,Javascript的事件分為同步任務和非同步任務.
- 2,遇到同步任務就放在執行棧中執行.
- 3,遇到非同步任務就放到任務佇列之中,等到執行棧執行完畢之後再去執行任務佇列之中的事件.
上面的步驟說得沒錯,很對,但是太淺了.現在的面試應該不會這麼簡單,起碼得加上巨集任務,微任務.再整上幾個Promise,async await,讓你判斷.否則都不好意思叫面試題!
為了避免被面試官吊起來打的情況,我們現在來詳細地理一理Event Loop.
Event loop 相關概念
JS呼叫棧
Javascript 有一個 主執行緒(main thread)和 呼叫棧(call-stack),所有的程式碼都要通過函式,放到呼叫棧(也被稱為執行棧)中的任務等待主執行緒執行。
JS呼叫棧採用的是後進先出的規則,當函式執行的時候,會被新增到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
WebAPIs
MDN的解釋: Web 提供了各種各樣的 API 來完成各種的任務。這些 API 可以用 JavaScript 來訪問,令你可以做很多事兒,小到對任意 window 或者 element做小幅調整,大到使用諸如 WebGL 和 Web Audio 的 API 來生成複雜的圖形和音效。
總結: 就是瀏覽器提供一些介面,讓JavaScript可以呼叫,這樣就可以把任務甩給瀏覽器了,這樣就可以實現非同步了!
任務佇列(Task Queue)
"任務佇列"是一個先進先出的資料結構,排在前面的事件,優先被主執行緒讀取。主執行緒的讀取過程基本上是自動的,只要執行棧一清空,"任務佇列"上第一位的事件就自動進入主執行緒。但是,如果存在"定時器",主執行緒首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主執行緒。
同步任務和非同步任務
Javascript單執行緒任務被分為同步任務和非同步任務.
- 同步任務會在呼叫棧 中按照順序等待主執行緒依次執行.
- 非同步任務會甩給在WebAPIs處理,處理完後有了結果後,將註冊的回撥函式放入任務佇列中等待主執行緒空閒的時候(呼叫棧被清空),被讀取到棧內等待主執行緒的執行。
巨集任務(MacroTask)和 微任務(MicroTask)
在JavaScript
中,任務被分為兩種,一種巨集任務(MacroTask
)也叫Task
,一種叫微任務(MicroTask
)。
巨集任務(MacroTask)
script(整體程式碼)
、setTimeout
、setInterval
、setImmediate
(瀏覽器暫時不支援,只有IE10支援,具體可見MDN
)、I/O
、UI Rendering
。
微任務(MicroTask)
Process.nextTick(Node獨有)
、Promise
、Object.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
在底層轉換成了 promise
和 then
回撥函式。
也就是說,這是 promise
的語法糖。
每次我們使用 await
, 直譯器都建立一個 promise
物件,然後把剩下的 async
函式中的操作放到 then
回撥函式中。
async/await
的實現,離不開 Promise
。從字面意思來理解,async
是“非同步”的簡寫,而 await
是 async wait
的簡寫可以認為是等待非同步方法執行完成。
關於73以下版本和73版本的區別
- 在73版本以下,先執行
promise1
和promise2
,再執行async1
。 - 在73版本,先執行
async1
再執行promise1
和promise2
。
主要原因是因為在谷歌(金絲雀)73版本中更改了規範,如下圖所示:
- 區別在於
RESOLVE(thenable)
和之間的區別Promise.resolve(thenable)
。
在73以下版本中
- 首先,傳遞給
await
的值被包裹在一個Promise
中。然後,處理程式附加到這個包裝的Promise
,以便在Promise
變為fulfilled
後恢復該函式,並且暫停執行非同步函式,一旦promise
變為fulfilled
,恢復非同步函式的執行。 - 每個
await
引擎必須建立兩個額外的 Promise(即使右側已經是一個Promise
)並且它需要至少三個microtask
佇列ticks
(tick
為系統的相對時間單位,也被稱為系統的時基,來源於定時器的週期性中斷(輸出脈衝),一次中斷表示一個tick
,也被稱做一個“時鐘滴答”、時標。)。
引用賀老師知乎上的一個例子
async function f() {
await p
console.log('ok')
}
複製程式碼
簡化理解為:
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
複製程式碼
- 如果
RESOLVE(p)
對於p
為promise
直接返回p
的話,那麼p
的then
方法就會被馬上呼叫,其回撥就立即進入job
佇列。 - 而如果
RESOLVE(p)
嚴格按照標準,應該是產生一個新的promise
,儘管該promise
確定會resolve
為p
,但這個過程本身是非同步的,也就是現在進入job
佇列的是新promise
的resolve
過程,所以該promise
的then
不會被立即呼叫,而要等到當前job
佇列執行到前述resolve
過程才會被呼叫,然後其回撥(也就是繼續await
之後的語句)才加入job
佇列,所以時序上就晚了。
谷歌(金絲雀)73版本中
- 使用對
PromiseResolve
的呼叫來更改await
的語義,以減少在公共awaitPromise
情況下的轉換次數。 - 如果傳遞給
await
的值已經是一個Promise
,那麼這種優化避免了再次建立Promise
包裝器,在這種情況下,我們從最少三個microtick
到只有一個microtick
。
詳細過程:
73以下版本
- 首先,列印
script start
,呼叫async1()
時,返回一個Promise
,所以列印出來async2 end
。 - 每個
await
,會新產生一個promise
,但這個過程本身是非同步的,所以該await
後面不會立即呼叫。 - 繼續執行同步程式碼,列印
Promise
和script end
,將then
函式放入微任務佇列中等待執行。 - 同步執行完成之後,檢查微任務佇列是否為
null
,然後按照先入先出規則,依次執行。 - 然後先執行列印
promise1
,此時then
的回撥函式返回undefinde
,此時又有then
的鏈式呼叫,又放入微任務佇列中,再次列印promise2
。 - 再回到
await
的位置執行返回的Promise
的resolve
函式,這又會把resolve
丟到微任務佇列中,列印async1 end
。 - 當微任務佇列為空時,執行巨集任務,列印
setTimeout
。
谷歌(金絲雀73版本)
- 如果傳遞給
await
的值已經是一個Promise
,那麼這種優化避免了再次建立Promise
包裝器,在這種情況下,我們從最少三個microtick
到只有一個microtick
。 - 引擎不再需要為
await
創造throwaway Promise
- 在絕大部分時間。 - 現在
promise
指向了同一個Promise
,所以這個步驟什麼也不需要做。然後引擎繼續像以前一樣,建立throwaway Promise
,安排PromiseReactionJob
在microtask
佇列的下一個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 │
└───────────────────────┘
複製程式碼
Node
的Event loop
一共分為6個階段,每個細節具體如下:
timers
: 執行setTimeout
和setInterval
中到期的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單執行緒的本質。
感謝及參考
- 一次弄懂Event Loop(這篇文章是寫得真的非常詳細,應該是全網最全的了,如果看完我寫的,還不太理解的,強烈建議看這篇)
- event loop一篇文章足矣