做者:Horace
博客:最近簡單的搭了一個博客~
ps:新人小白一枚,若有錯誤,歡迎指出~前端
筆者最近忙着作項目之類的,文章輸出遺落下了一段時間,此次咱們就來聊一個面試中一個比較重要的知識點 —— Event Loopnode
可能有人會奇怪一個 EventLoop 還能寫出什麼,且聽我慢慢來逼叨,看完這篇文章帶你搞定 Event Loop 以及它相關的一些知識點。git
在開始說 Event Loop 以前,咱們先來認識一下它究竟是個什麼東西。程序員
In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), then calls the relevant event handler ("dispatches the event"). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.github
上面這段是Wikipedia對 Event Loop 的解釋,簡單的來講就是Event Loop是一個程序結構,用於等待和分派消息和事件
我我的的理解是 JS 中的 Event Loop 是瀏覽器或 Node 的一種協調 JavaScript 單線程運行時不會阻塞的一種機制。面試
可能有人會比較疑惑前端爲何要學看起來比較底層的 Event Loop,不只僅是由於這是一道面試的常考題。chrome
上文我說了 Event Loop 是單線程阻塞問題的一種解決機制,因此在正式開始前仍是要先從進程和線程的角度來聊一聊。衆所周知的一件事是,JavaScript 是一個單線程機制的語言,那咱們先來看看進程和線程的定義:promise
說實話,光從定義來看你根本感覺不到進程和線程究竟是什麼樣的一個東西。簡單來講,進程簡單理解就是咱們日常使用的程序,如 QQ,瀏覽器,網盤等。進程擁有本身獨立的內存空間地址,擁有一個或多個線程,而線程就是對進程粒度的進一步劃分。瀏覽器
更通俗的來講,進程就像是一家工廠,多個工廠之間是獨立存在的。而線程就像是工廠中的那些工人,共享資源,完成同一個大目標。網絡
不少人都知道的是,JavaScript 是一門動態的解釋型的語言,具備跨平臺性。在被問到 JavaScript 爲何是一門單線程的語言,有的人可能會這麼回答:「語言特性決定了 JavaScript 是一個單線程語言,JavaScript 天生是一個單線程語言」,這只不過是一層糖衣罷了。
JavaScript 從誕生起就是單線程,緣由大概是不想讓瀏覽器變得太複雜,由於多線程須要共享資源、且有可能修改彼此的運行結果,對於一種網頁腳本語言來講,這就太複雜了。
準確的來講,我認爲 JavaScript 的單線程是指 JavaScript 引擎是單線程的,JavaScript 的引擎並非獨立運行的,跨平臺意味着 JavaScript 依賴其運行的宿主環境 --- 瀏覽器(大部分狀況下是瀏覽器)。
瀏覽器須要渲染 DOM,JavaScript 能夠修改 DOM 結構,JavaScript 執行時,瀏覽器 DOM 渲染中止。若是 JavaScript 引擎線程不是單線程的,那麼能夠同時執行多段 JavaScript,若是這多段 JavaScript 都操做 DOM,那麼就會出現 DOM 衝突。
舉個例子來講,在同一時刻執行兩個 script 對同一個 DOM 元素進行操做,一個修改 DOM,一個刪除 DOM,那這樣話瀏覽器就會懵逼了,它就不知道到底該聽誰的,會有資源競爭,這也是 JavaScript 單線程的緣由之一。
以前說過,JavaScript 運行的宿主環境瀏覽器是多線程的。
以 Chrome 來講,咱們能夠經過 Chrome 的任務管理器來看看。
當你打開一個 Tab 頁面的時候,就建立了一個進程。若是從一個頁面打開了另外一個頁面,打開的頁面和當前的頁面屬於同一站點的話,那麼這個頁面會複用父頁面的渲染進程。
這裏沒看懂不要緊,後面我會再說。
看到這裏,總算是進入正題了,先講講瀏覽器端的 Event Loop 是什麼樣的。
上圖是一張 JS 的運行機制圖,Js 運行時大體會分爲幾個部分:
說到這裏,Event Loop 也能夠理解爲:不斷地從任務隊列中取出任務執行的一個過程。
上文已經說過了 JavaScript 是一門單線程的語言,一次只能執行一個任務,若是全部的任務都是同步任務,那麼程序可能由於等待會出現假死狀態,這對於一個用戶體驗很強的語言來講是很是不友好的。
好比說向服務端請求資源,你不可能一直不停的循環判斷有沒有拿到數據,就好像你點了個外賣,點完以後就開始一直打電話問外賣有沒有送到,外賣小哥都會抄着鍋鏟來打你(狗頭)。所以,在 JavaScript 中任務有了同步任務和異步任務,異步任務經過註冊回調函數,等到數據來了就通知主程序。
簡單的介紹一下同步任務和異步任務的概念。
從概念就能夠看出來,異步任務從必定程度上來看比同步任務更高效一些,核心是提升了用戶體驗。
Event Loop 很好的調度了任務的運行,宏任務和微任務也知道了,如今咱們就來看看它的調度運行機制。
JavaScript 的代碼執行時,主線程會從上到下一步步的執行代碼,同步任務會被依次加入執行棧中先執行,異步任務會在拿到結果的時候將註冊的回調函數放入任務隊列,當執行棧中的沒有任務在執行的時候,引擎會從任務隊列中讀取任務壓入執行棧(Call Stack)中處理執行。
如今就有一個問題了,任務隊列是一個消息隊列,先進先出,那就是說,後來的事件都是被加在隊尾等到前面的事件執行完了纔會被執行。若是在執行的過程當中忽然有重要的數據須要獲取,或是說有事件忽然須要處理一下,按照隊列的先進先出順序這些是沒法獲得及時處理的。這個時候就催生了宏任務和微任務,微任務使得一些異步任務獲得及時的處理。
曾經看到的一個例子很好,宏任務和微任務形象的來講就是:你去營業廳辦一個業務會有一個排隊號碼,當叫到你的號碼的時候你去窗口辦充值業務(宏任務執行),在你辦理充值的時候你又想改個套餐(微任務),這個時候工做人員會直接幫你辦,不可能讓你從新排隊。
因此上文說過的異步任務又分爲宏任務和微任務,JS 運行時任務隊列會分爲宏任務隊列和微任務隊列,分別對應宏任務和微任務。
先介紹一下(瀏覽器環境的)宏任務和微任務大體有哪些:
總的來講就是:同步任務/宏任務 -> 執行產生的全部微任務(包括微任務產生的微任務) -> 同步任務/宏任務 -> 執行產生的全部微任務(包括微任務產生的微任務) -> 循環......
注意:微任務隊列
光說不練假把式,如今就來看一個例子:
放圖的緣由是爲了讓你們在看解析以前能夠先本身按照運行順序走一遍,寫好答案以後再來看解析。
解析:
(用綠色的表示同步任務和宏任務,紅色表示微任務)
+ console.log('script start')
+ setTimeout(function() {
+ console.log('setTimeout')
+ }, 0)
+ new Promise((resolve, reject)=>{
+ console.log("promise1")
+ resolve()
+ })
- .then(()=>{
- console.log("then11")
+ new Promise((resolve, reject)=>{
+ console.log("promise2")
+ resolve();
+ })
- .then(() => {
- console.log("then2-1")
- })
- .then(() => {
- console.log("then2-2")
- })
- })
- .then(()=>{
- console.log("then12")
- })
+ console.log('script end')
複製代碼
script start
promise1
,而後 resolvescript end
,當前執行棧清空then11
promise2
,而後 resolvethen2-1
,觸發 promise2 的第二個 then,註冊到微任務隊列[then12, then2-2]then12
then2-2
setTimeout
通過以上一番縝(xia)密(gao)分析,但願沒有繞暈你,最後的輸出結果就是:
script start -> promise1 -> script end -> then11 -> promise2 -> then2-1 -> then12 -> then2-2 -> setTimeout
不知道你們看了宏任務和微任務以後會不會有一個疑惑,宏任務和微任務都是異步任務,微任務以前說過了是爲了及時解決一些必要事件而產生的。
爲何要有微任務?
爲何要有微任務的緣由前面已經說了,這裏就再也不贅述,簡單說一下就是爲了及時處理一些任務,否則等到最後再執行的時候拿到的數據可能已是被污染的數據達不到預期目標了。
什麼是宏任務?什麼是微任務?
相信你們在學習 Event Loop 查找資料的時候,確定各類資料裏面都會講到宏任務和微任務,可是不知道你有沒有靈魂拷問過你本身:什麼是宏任務?什麼是微任務?怎麼區分宏任務和微任務?
不能只是默許接受這個概念,在這裏,我根據個人我的理解進行一番說(hu)明(che)
宏任務和微任務的真面目
其實在 Chrome 的源碼中並無什麼宏任務和微任務的代碼或是說明,在 JS 大會上提到過微任務這個名詞,可是也沒有說到底什麼是微任務。
宏任務
文章最開始的時候說過,在 chrome 裏,每一個頁面都對應一個進程。而該進程又有多個線程,好比 JS 線程、渲染線程、IO 線程、網絡線程、定時器線程等等,這些線程之間的通訊是經過向對象的任務隊列中添加一個任務(postTask)來實現的。宏任務的本質能夠認爲是多線程事件循環或消息循環,也就是線程間通訊的一個消息隊列。
就拿 setTimeout 舉例來講,當遇到它的時候,瀏覽器就會對 Event Loop 說:嘿,我有一個任務交給你,Event Loop 就會說:好的,我會把它加到個人 todoList 中,以後我會執行它,它是須要調用 API 的。
宏任務的真面目是瀏覽器派發,與 JS 引擎無關的,參與了 Event Loop 調度的任務
微任務
微任務是在運行宏任務/同步任務的時候產生的,是屬於當前任務的,因此它不須要瀏覽器的支持,內置在 JS 當中,不須要 API 支持,直接在 JS 的引擎中就被執行掉了。
async 隱式返回 Promise 做爲結果
執行完 await 以後直接跳出 async 函數,讓出執行的全部權
當前任務的其餘代碼執行完以後再次得到執行權進行執行
當即 resolve 的 Promise 對象,是在本輪"事件循環"的結束時執行,而不是在下一輪"事件循環"的開始時
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')
複製代碼
按照以前的分析方法去分析以後就會得出一個結果:
script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
能夠看出 async1 函數獲取執行權是做爲微任務的隊尾,可是,在 Chrome73(金絲雀) 版本以後,async 的執行優化了,它會在 promise1 和 promise2 的輸出以前執行。筆者大概瞭解了一下應該是用 PromiseResolve 對 await 進行了優化,減小了 Promise 的再次建立,有興趣的小夥伴能夠看看 Chrome 的源碼。
Node 中也有宏任務和微任務,與瀏覽器中的事件循環相似。Node 與瀏覽器事件循環不一樣,其中有多個宏任務隊列,而瀏覽器是隻有一個宏任務隊列。
Node 的架構底層是有 libuv,它是 Node 自身的動力來源之一,經過它能夠去調用一些底層操做,Node 中的 Event Loop 功能就是在 libuv 中封裝實現的。
Node 中的宏任務和微任務在瀏覽器端的 JS 相比增長了一些,這裏只列出瀏覽器端沒有的:
Node 的事件循環分紅了六個階段,每一個階段對應一個宏任務隊列,至關因而宏任務進行了一個分類。
執行的輪循順序 --- 每一個階段都要等對應的宏任務隊列執行完畢纔會進入到下一個階段的宏任務隊列
每兩個階段之間執行微任務隊列
這裏要注意的是,nextTick 事件是一個單獨的隊列,它的優先級會高於微任務,因此在當前宏任務/同步任務執行完成以後,會先執行 nextTick 隊列中的全部任務,再去執行微任務隊列中的全部任務。
在這裏要單獨說一下 setTimeout 和 setImmediate,setTimeout 定時器很熟悉,那就說說 setImmediate
setImmediate() 方法用於把一些須要長時間運行的操做放在一個回調函數裏,並在瀏覽器完成其餘操做(如事件和顯示更新)後當即運行回調函數。從定義來看就是爲了防止一些耗時長的操做阻塞後面的操做,這也是爲何 check 階段運行順序排的比較後。
咱們來看這樣的一個例子:
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
複製代碼
這裏涉及 timers 階段和 check 階段,按照上面的運行順序來講,timers 階段是在第一個執行的,會早於 check 階段。運行這段程序能夠看到以下的結果:
但是再多運行幾回,你就會看到以下的結果:
setImmediate 的輸出跑到 setTimeout 前面去了,這時候就是:小朋友你是否有不少的問號❓
咱們來分析一下緣由,timers 階段確實是在 check 階段以前,可是在 timers 階段時候,這裏的 setTimeout 真的到了執行的時間嗎?
這裏就要先看看 setTiemout(fn, 0)
,這個語句的意思不是指不延遲的執行,而是指在能夠執行 setTimeout 的時候就當即執行它的回調,也就是處理完當前事件的時候當即執行回調。
在 Node 中 setTimeout 第二個時間參數的最小值是 1ms,小於 1ms 會被初始化爲 1(瀏覽器中最小值是 4ms),因此在這裏 setTimeout(fn, 0) === setTimeout(fn, 1)
setTimeout 的回調函數在 timers 階段執行,setImmediate 的回調函數在 check 階段執行,Event Loop 的開始會先檢查 timers 階段,可是在代碼開始運行以前到 timers 階段(代碼的啓動、運行)會消耗必定的時間,因此會出現兩種狀況:
timers 前的準備時間超過 1ms,知足 loop -> timers >= 1,setTimeout 的時鐘週期到了,則執行 timers 階段(setTimeout)的回調函數
timers 前的準備時間小於 1ms,還沒到 setTimeout 預設的時間,則先執行 check 階段(setImmediate)的回調函數,下一次 Event Loop 再進入 timers 階段執行 timer 階段(setTimeout)的回調函數
最開始就說了,一個優秀的程序員要讓本身的代碼按照本身想要的順序運行,下面咱們就來控制一下 setTimeout 和 setImediate 的運行。
const start = Date.now()
while (Date.now() - start < 10)
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
複製代碼
讓 setImmediate 先執行
setImmediate 是在 check 階段執行,相對於 setTimeout 來講是在 timers 階段以後,只須要想辦法把程序的運行環境控制在 timers 階段以後就能夠了。
讓程序至少從 I/O callbacks 階段開始 --- 能夠套一層文件讀寫把把程序控制在 I/O callbacks 階段的運行環境中👇
const fs = require('fs')
fs.readFile(__dirname, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
複製代碼
timers 階段的執行有所變化
setTimeout(() => console.log('timeout1'))
setTimeout(() => {
console.log('timeout2')
Promise.resolve().then(() => console.log('promise resolve'))
})
複製代碼
node 10 及以前的版本:
要考慮上一個定時器執行完成時,下一個定時器是否到時間加入了任務隊列中,若是未到時間,先執行其餘的代碼。
好比:
timer1 執行完以後 timer2 到了任務隊列中,順序爲 timer1 -> timer2 -> promise resolve
timer2 執行完以後 timer2 還沒到任務隊列中,順序爲 timer1 -> promise resolve -> timer2
node 11 及其以後的版本:
timeout1 -> timeout2 -> promise resolve
一旦執行某個階段裏的一個宏任務以後就馬上執行微任務隊列,這和瀏覽器端運行是一致的。
Node 和瀏覽器端有什麼不一樣
看到這裏,你應該對瀏覽器端和 Node 端的 Event Loop 有了必定的瞭解,那就留一個題目。
不直接放代碼是想讓你們先本身思考而後在敲代碼運行一遍~
本文到這裏算是結束了,仍是那句話,作一個程序員要知其然更要知其因此然。我寫些文章也是想把知識輸出,檢驗本身是否是真的學懂了。文章中可能還存在一些沒有說清楚的地方或者是有錯的地方,歡迎直接指出~
Tasks, microtasks, queues and schedules
JS大會1
JS大會2
《深刻淺出nodejs》
我把個人學習記錄都記錄在了個人 github 而且會持續的更新下去,有興趣的小夥伴能夠看看~
github