JavaScript 是單線程的,因爲單線程會形成I/O阻塞,好比發送請求時未響應就可能形成頁面停滯,爲了解決這個問題,瀏覽器開始支持異步JS,異步JS就是把一些異步任務(ajax、定時器)等放到任務隊列中,而後經過事件循環不斷讀取、觸發任務隊列中的異步代碼,這種機制就叫作事件循環Event Loop。node
Event Loop的核心代碼是採用c++寫的(屬於NodeJs的範疇),本質上來講Event Loop就是採用輪詢的方式不斷讀取、執行事件,今天咱們要討論的就是事件循環中的細節。c++
階段Event Loop內部分爲如下階段面試
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘複製代碼
上面的每個階段都有一個隊列(先進先出),裏面存放回調函數。每當Event Loop到達一個階段,通常來講都會執行隊列中的某些函數(也有可能不操做)ajax
一個 Node.js 程序結束時,Node.js 會檢查 event loop 是否在等待異步 I/O 操做結束,是否在等待計時器觸發,若是沒有,就會關掉 event loop。promise
這個階段頗有多是Event Loop開始的第一個階段,主要存放setTimeout或者setInterval等宏任務瀏覽器
這個階段主要用來獲取新的I/O事件,當 event loop 進入 poll 階段,發現 poll 隊列爲空,event loop 檢查了一下最近的計時器,大概還有 100 毫秒時間,因而 event loop 決定這段時間就停在 poll 階段,當定時器任務快開始的時候,Event Loop會繞過poll階段進入check階段異步
這個階段有一個API,面試的時候常常用的到,屬於nodeJS的setImmediate,它一樣屬於宏任務,可是相對於定時器來講,它的特色是要求更快執行socket
setImmediate和setTimeoutsetImmediate 和 setTimeout 很類似,可是其回調函數的調用時機卻不同。async
從所屬的階段隊列來看,setImmediate屬於check階段,setTimeout屬於timers階段,那麼二者之間到底誰先執行呢?ide
先看一段代碼
setTimeout(()=>{console.log('timeout')},0) setImmediate(()=>{console.log('immediate')},0)複製代碼
通常來講,都會優先執行setTmmediate,可是上面的代碼實際執行順序是這樣的爲何會有時先執行timeout,有時先執行immediate呢,這要從順序看起,若是上面的代碼setTimeout的時間設定爲1000ms,那你們必定不會感到困惑,因爲immediate存在於check階段,當時間設定爲1000ms時,Event Loop處於poll階段,畢竟要等到1000ms才執行timers隊列中的函數,因此Loop打算休息一下。
而後呢好像時間差很少了,Loop發現check階段有個immediate函數,因而跑過去執行一下,執行完了就再跑到timers階段執行。
而上面產生困惑的最大緣由是定時器設置時間爲0,這就要看Event Loop開始時,所處的階段。
若是Event Loop此時在timers階段,隊列中尚未定時器任務,又或者定時器任務還沒到時間,那麼必然會跳過此階段,優先執行immediate任務。
若是此時有任務,並且時間到了,那麼必然會先執行setTimeout,這也是上述代碼產生困惑的緣由。
下面咱們對它進行改寫
setTimeout(() => { setTimeout(() => {console.log("timeout"); }, 0); setImmediate(() => {console.log("immediate"); }); }, 1000);複製代碼
上面的代碼,1秒後,執行箭頭函數,此時Event Loop並不在timers階段,因爲順序是不可變的,因此老是會優先執行immediate
process.nextTick()你可能發現 process.nextTick() 這個重要的異步 API 沒有出如今任何一個階段裏,那是由於從技術上來說 process.nextTick() 並非 event loop 的一部分。實際上,無論 event loop 當前處於哪一個階段,nextTick 隊列都是在當前階段後就被執行了。
setTimeout(() => { setTimeout(() => {console.log("timeout"); }, 0); setImmediate(() => {console.log("immediate"); }); process.nextTick(()=>{ console.log('nexTick') }) }, 1000);複製代碼
上面的代碼執行順序是這樣的
nextTick是在當前階段立刻執行,因爲上面的代碼執行後Loop處於poll階段,因此會優先執行nextTick
爲了更好得實驗,咱們再改一下代碼
setTimeout(() => { setTimeout(() => {console.log("timeout"); process.nextTick(() => { console.log("nexTick2"); }); }, 0); setImmediate(() => {console.log("immediate"); }); process.nextTick(() => {console.log("nexTick"); }); }, 1000);複製代碼
下面是結果,能夠發現nextTick是在當前階段立刻執行的
nexTick immediate timeout nexTick2複製代碼process.nextTick() 和 setImmediate()
這兩個函數功能很像,並且名字也很使人疑惑。
process.nextTick() 的回調會在當前 event loop 階段「當即」執行。 setImmediate() 的回調會在後續的 event loop 週期(tick)執行。
兩者的名字應該互換纔對。process.nextTick() 比 setImmediate() 更 immediate(當即)一些。
這是一個歷史遺留問題,並且爲了保證向後兼容性,也不太可能獲得改善。因此就算這兩個名字聽起來讓人很疑惑,也不會在將來有任何變化。
咱們推薦開發者在任何狀況下都使用 setImmediate(),由於它的兼容性更好,並且它更容易理解。
宏任務和微任務異步任務中分宏任務和微任務,微任務老是比宏任務先執行
setTimeout(()=> console.log(4))//宏任務new Promise(resolve => { resolve()//同步任務 console.log(1) //同步任務}).then(()=> { console.log(3) //微任務})console.log(2) //同步任務複製代碼
改形成await
setTimeout(_ => console.log(4)) //宏任務async function main() { console.log(1) //同步任務 await Promise.resolve() //同步任務 至關於 resolve() console.log(3) //至關於promise.then //微任務} main()console.log(2) //同步任務複製代碼