Eventloop的祕密

非常兔發表於2018-08-10

Eventloop的祕密

寫在前面
面過前端的小夥伴們都見過這麼一道關於非同步的小題:

for(var i = 0; i<10; i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}
複製程式碼

稍微瞭解非同步的同學都會對答案呼之欲出。BUT! 如果問題升級為:

setTimeout(function(){
    console.log(1)
},0)
new Promise(function executor(resolve) {
  console.log(2)
  for(var j = 0;j<100;j++){
    j=99&&resolve()
  }
  console.log(3)
}).then(function(){
    console.log(4)
})
console.log(5)
複製程式碼

是不是稍微的有那麼點小蒙圈? 彆著急,本篇內容結束後以上問題都不再是事兒。解決以上問題的要點,首先需要清楚Javascript非同步處理模組,事件佇列,以及事件環-Eventloop.

基礎概念

程式 (process)與執行緒 (thread)

程式是作業系統分配資源和排程任務的基本單位,執行緒是建立在程式上的一次程式執行單位,一個程式上可以有多個執行緒。
程式和執行緒屬基礎概念,不再贅述。有個最生動易懂的解釋,詳情請移步參考:程式與執行緒的一個簡單解釋

Javascript 單執行緒

對Javascript而言,從誕生之日起,它就是單執行緒的。為什麼呢?舉個小栗子:如果可以多執行緒,a執行緒要新增某DOM節點,b執行緒要刪除它,瀏覽器怎麼辦?難道要精分? 所以說,單執行緒減少了很多情境的複雜性。
既然js是單執行緒的,它又以什麼樣的規則來處理併發的任務呢?千軍萬馬要過獨木橋的時候,不能靠力氣來搶路。單執行緒,任務多,就得有個規矩來安排大家。於是祕密終於被我們發現——事件環(Event Loop)就要出馬了。 對於首次聽說這個概念的同學,有必要鋪墊下基礎知識:

堆(heap)

物件被分配在一個堆中,即用以表示一個大部分非結構化的記憶體區域。

棧(stack)

函式呼叫形成一個棧幀;

棧的特點:先進後出(First in, last out,具體是怎樣讓那些函式先入後出的?看下圖會恍然大明白,圖中的帥哥是Philip Roberts,看解釋,別光看臉!

Eventloop的祕密
gif圖有點大,如果圖裂了,請直接看Philip Roberts的演講:Help, I'm stuck in an event-loop

任務佇列(queue)—— 特點:先進先出(FIFO)

一個 JavaScript 執行時包含了一個待處理的訊息佇列。 每一個訊息都有一個為了處理這個訊息相關聯的函式

說到了任務佇列,就到了重點部分:事件環(Eventloop)了

Defined by webappapis:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section.

任務佇列以事件環來協調事件,使用者互動,指令碼,渲染,網路等。

事件環 Eventloop

  • Explained by Jake Archibald

    Each 'thread' gets its own event loop, so each web worker gets its own, so it can execute independently

  • Or explained by some other guys:

    This is a constantly running process that checks if the call stack is empty. Imagine it like a clock and every time it ticks it looks at the Call Stack and if it is empty it looks into the Event Queue.

簡單來說,每個執行緒都有他自己的事件環,瀏覽器也擁有自己的事件環;事件環是一種執行時機制,它像個鐘錶一樣,每滴答一下,就去看看stack裡有沒有事需要處理,沒有的話就去事件佇列(Event Queue)看看有沒有事做。
此處大家需要明白,事件環並不是定死的某個規矩,需要根據不同的執行時進行自己的一套規則。

There are two kinds of event loops: those for browsing contexts, and those for workers.--from webapis.

node下的事件環瀏覽器環境下的事件環就不是相同的規則。一定要記清楚哦!首先討論瀏覽器事件環。

瀏覽器事件環


依據webapis裡宣告的事件環(event loop)存在時的執行步驟,概括如下:

  1. 首先執行script,script被稱為全域性任務,也屬於macrotask;
  2. 當macrotask執行完以下,執行所有的微任務;
  3. 微任務全部執行完,再取任務佇列中的一個巨集任務執行。
  • 巨集任務包括:script, setTimeout, setInterval, setImmediate, I/O
  • 微任務包括:process.nextTick(node api), 原生Promise(有些實現的promise將then方法放到了巨集任務中),Object.observe(已廢棄), MutationObserver

一圖頂千言,作圖解釋下:

Eventloop的祕密

看似按照這個規則,我們文章開頭的問題就可以有答案了。 回顧一下:

setTimeout(function(){
    console.log(1)
},0)
new Promise(function executor(resolve) {
  console.log(2)
  for(var j = 0;j<100;j++){
    j=99&&resolve()
  }
  console.log(3)
}).then(function(){
    console.log(4)
})
console.log(5)
複製程式碼

程式碼執行後,按照執行順序:

  1. script執行:2,3,5(均為同步任務,new Promise會在同步程式碼中執行)
  2. 處理微任務佇列中的所有任務:then 4
  3. 接著執行下一個巨集任務: setTimeout 1 所以執行結果為: 2,3,5,4,1

微任務

Explained by webappapis

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.

翻譯:每個eventloop都有一個微任務佇列,微任務最初是被放到微任務佇列而不是任務佇列

這裡大家請把這句話放在心裡,這是解決巨集任務的一把鑰匙所在。

巨集任務到底是什麼

雖然知道任務佇列分為巨集任務,微任務,但是一直未找到巨集任務的定義,直到看到stackoverflow上的解釋。

One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification).

每個事件環必須有一個來自巨集任務佇列的任務正在執行。 這裡將巨集任務解釋為whatwg上定義的任務佇列.
是不是有點迷惑,這不對啊? 雖然把任務佇列分成巨集任務,微任務理解比較容易,但我認為這後面更大的祕密才是我苦苦找尋的真相。 如果只執行巨集任務佇列,那微任務佇列怎麼執行呢?
這正好與上文中提到微任務佇列是單獨的佇列,跟任務佇列不是一回事符合。 整個任務可以理解為,所有的巨集任務放在一個巨集任務佇列(即任務佇列),處理完一個巨集任務(從sccript開始),將微任務佇列(包含當時所有的微任務)壓入任務佇列(巨集任務佇列)並執行,之後再取下一個任務佇列(巨集任務)中的巨集任務。

於是,我們的題目可以翻譯成以下解決思路:

  1. 巨集任務佇列中script執行:2,3,5
  2. 微任務佇列壓入巨集任務佇列(任務佇列)並執行:4
  3. 取下一個巨集任務執行:1

多說一句

事件環eventloop中為什麼必須在所有的微任務microtask都執行結束後再取新的巨集任務macrotask呢?

這涉及microtask執行機制

Eventloop的祕密

step2中做了明確的解釋,當微任務佇列 的標記被寫為true之後,只要microtask的佇列不為空,eventloop中的當前任務就會按順序執行microtask佇列中的所有任務。
這裡可以看到兩者的一點區別,微任務microtask佇列是獨立的一個佇列,在eventloop執行過程中才進入到任務佇列task queue一次執行。

寫在最後

沒有盡興的朋友推薦以下幾篇好文,這些是我個人認為講解事件環,非同步事件佇列等最為具體清晰的文章:
Best reference:

  1. Tasks, microtasks, queues and schedules by Jake Archibald
  2. How does Javascript event work? or
    Help, I'm stuck in an event-loop by Philip Roberts
  3. stackoverflow.com/questions/2…

其他參考

  1. microtasks-macrotasks-more-on-the-event-loop
  2. event-loop-timers-and-nexttick
  3. node process_nextTick_api
  4. microtask-queue
  5. webapis:task-queue
  6. webapis:event-loops

希望大家看文字文能有收貨。歡迎批評指正。

Author: Yanni Jia
Nickname: 非常兔
Email: 385067638@qq.com

相關文章