JS中的事件循環和任務隊列

JS引擎(The JavaScript Engine)

JavaScript引擎的一個流行示例是Google的V8引擎。V8引擎被用在了Chrome和Nodejs裏。
imagejavascript

運行時(The Runtime)

瀏覽器中有不少幾乎每一個開發都調用過的API,好比 setTimeout等,但引擎不提供這些API。
imagejava

JS是單線程的併發語言,這就意味着,在一個時間段內,它只能處理一項任務或執行一段代碼。它有一個單一的調用棧(Single Call Stack),和堆(Heap),隊列(Queue)組成的JS併發模型(Javascript Concurrentcy Model).
image.png
[可視化表示]promise

1. 調用棧(Call Stack) : 在程序中,它是一個記錄程序調用的數據結構。每一個數據結構,也可稱爲棧幀。
來看一下MDN上的例子:瀏覽器

function foo(b) {
  var a = 5;
  return a * b + 10;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(6)); // 返回 100

當調用bar時,建立了第一個幀 ,幀中包含了bar的參數和局部變量。當bar調用foo時,第二個幀就被建立,並被壓到第一個幀之上,幀中包含了foo的參數和局部變量。當foo返回時,最上層的幀就被彈出棧(剩下bar函數的調用幀 )。當bar返回的時候,棧就空了。
再加一張動態圖:image服務器

咱們有時會在瀏覽器的控制檯看到長長的紅色錯誤堆棧跟蹤,它基本上指示了當前調用堆棧的狀態,以及該函數在堆棧中從上到下失敗的方式。網絡

function foo(){
throw new Error("Oops!");
}
function bar(){
    foo();
}
function baz() {
    bar();
}
baz();

image.png
[Chrome瀏覽器]session

有時,咱們進入函數的無限循環,也會拋出錯誤。在Chrome中,棧裏的最大深度爲16,000。
image數據結構

2.堆(Heap):對象被分配在一個堆中,即用以表示一大塊非結構化的內存區域。
3.隊列(Queue):一個 JavaScript 運行時包含了一個待處理的消息隊列。每個消息都關聯着一個用以處理這個消息的函數。多線程

在事件循環期間的某個時刻,運行時從最早進入隊列的消息開始處理隊列中的消息。爲此,這個消息會被移出隊列,並做爲輸入參數調用與之關聯的函數。正如前面所提到的,調用一個函數老是會爲其創造一個新的棧幀。併發

函數的處理會一直進行到執行棧再次爲空爲止;而後事件循環將會處理隊列中的下一個消息(若是還有的話)。

Event Loop

基本上,當咱們評估 JS 代碼的性能時,堆棧中的函數會使其速度慢或快,但執行d成千上萬次迭代,或使用或執行超過數百萬行代碼的文件,速度將變慢,並且會保持堆棧佔用或阻止。這樣的代碼或文件稱爲阻止腳本(Blocking script).

網絡請求可能很慢,圖片請求可能很慢,但幸運的是,服務器請求能夠經過 AJAX(異步函數)來完成。咱們假設,這些網絡請求是經過同步函數進行的,那麼會發生什麼?網絡請求發送到某些服務器,不能就是另外一臺在某處的計算機。如今,計算機發送回響應的速度可能會很慢。同時,若是我單擊某個 CTA 按鈕,或者須要執行一些其餘渲染,則堆棧被阻止時不會執行任何操做。在多線程語言(如 Ruby)中,能夠處理它,但在 Javascript 等單線程語言中,除非堆棧中的函數返回值,不然這是不可能的。網頁將崩潰,由於瀏覽器不能作任何事情。若是咱們想要最終用戶的流暢 UI,這不是理想的選擇。咱們如何處理?

最簡單的解決方案是使用異步回調,這意味着咱們運行代碼的某些部分,並給它一個回調(函數),稍後將被執行。咱們都必定遇到異步回調,就像使用Node的任何AJAX請求同樣,都是關於異步函數執行的。全部這些異步回調不會當即運行,將會在稍後運行,所以不能當即推送到堆棧內,不像同步函數,如它們到底去哪裏,它們如何處理?$.get(),setTimeout(),setInterval(), Promises, etc.`console.log(), mathematical operations.`
image

從上圖中,網絡請求在執行過程:
一、請求函數被執行,傳遞一個匿名函數做爲回調,這個函數在未來某個時候,當響應(response)可用時執行。
二、「Script call done!」 被當即輸出到控制檯。
三、在未來某時刻,響應(response)從服務端返回,執行咱們的回調函數,並將其body輸出到控制檯。

調用方與響應的解耦使JavaScript運行時能夠在等待異步操做完成並觸發其回調前執行其餘操做。在瀏覽器中,用於處理異步事件,是由C++來實現的,例如DOM事件,http請求,setTimeout等(知道了這一點以後,在Angular 2中,使用了區域,這些區域對這些API進行了猴子修補,以引發運行時更改檢測,如今我能夠了解它們如何實現此目的。)在瀏覽器中,當這些API被調用時,瀏覽器將建立進程處理異步的回調函數。

猴子補丁:在程序運行的過程當中動態的修改一些模塊、類、方法,而不是在靜態代碼中去修改相應的實現。

瀏覽器Web API-由瀏覽器建立的線程,使用C ++實現,用於處理異步事件,例如DOM事件,http請求,setTimeout等。

因爲這些WebAPI自己不能將執行代碼放到堆棧上,若是這樣作了,它將隨機出如今代碼中間。因爲這些WebAPI自己不能將執行代碼放到堆棧上,若是這樣作了,它將隨機出如今代碼中間。 上面討論的消息回調隊列展示了方法。 任何WebAPI在執行完,都會將回調(function)推送到此隊列中。如今,事件循環(Event Loop)負責在隊列(Queue)中執行這些回調,並在其爲空時將其壓入堆棧(Stack)

進入隊列,並不會當即被執行,只有當前Event Loop執行棧中的任務被執行完成後,纔會被壓入執行棧。

事件循環(Event Loop)的基本工做是同時查看堆棧(Stack)和任務隊列(Queue),並在將堆棧視爲空時將隊列中的第一件事推入堆棧。 在處理任何其餘消息以前,將徹底處理每一個消息或回調。

while (queue.waitForMessage()) {  
queue.processNextMessage();  
}

image

在 Web 瀏覽器中,每當發生事件(Event)並附加事件偵聽器(Listener)時,都將添加消息(Message)。若是沒有偵聽器(Listener),則事件(Event)將丟失。所以,單擊具備 click 事件處理程序的元素(Element)將添加一條消息(Message) - 與任何其餘事件同樣。此回調函數(Callback function)的調用將用做調用堆棧中的初始幀,因爲 JavaScript 是單線程的,在堆棧上返回全部調用以前,將中止進一步的消息輪詢和處理。後續(同步)函數調用向堆棧添加新的調用幀。

如今能夠看出,有不少不一樣的任務隊列,由上面可知,通常可分爲兩類,1)宏任務,2)微任務。
隊列優先級
我先把結論COPY過來,有時間再寫一篇文章詳細說明。

小結
在JS引擎中,咱們能夠按性質把任務分爲兩類,macrotask(宏任務)和 microtask(微任務)。

瀏覽器JS引擎中:

macrotask(按優先級順序排列): script(你的所有JS代碼,「同步代碼」), setTimeout, setInterval, setImmediate, I/O,UI rendering
microtask(按優先級順序排列):process.nextTick,Promises(這裏指瀏覽器原生實現的 Promise), Object.observe, MutationObserver
JS引擎首先從macrotask queue中取出第一個任務,執行完畢後,將microtask queue中的全部任務取出,按順序所有執行;
而後再從macrotask queue(宏任務隊列)中取下一個,執行完畢後,再次將microtask queue(微任務隊列)中的所有取出;
循環往復,直到兩個queue中的任務都取完。
因此,瀏覽器環境中,js執行任務的流程是這樣的:

第一個事件循環,先執行script中的全部同步代碼(即 macrotask 中的第一項任務)
再取出 microtask 中的所有任務執行(先清空process.nextTick隊列,再清空promise.then隊列)
下一個事件循環,再回到 macrotask 取其中的下一項任務
再重複2
反覆執行事件循環…

NodeJS引擎中:

先執行script中的全部同步代碼,過程當中把全部異步任務壓進它們各自的隊列(假設維護有process.nextTick隊列、promise.then隊列、setTimeout隊列、setImmediate隊列等4個隊列)
按照優先級(process.nextTick > promise.then > setTimeout > setImmediate),選定一個  不爲空 的任務隊列,按先進先出的順序,依次執行全部任務,執行過程當中新產生的異步任務繼續壓進各自的隊列尾,直到被選定的任務隊列清空。
重複2...
也就是說,NodeJS引擎中,每清空一個任務隊列後,都會從新按照優先級來選擇一個任務隊列來清空,直到全部任務隊列被清空。

資料來源:

https://developer.mozilla.org...
https://medium.com/@gaurav.pa...
https://blog.sessionstack.com...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://blog.csdn.net/happyqy...
相關文章
相關標籤/搜索