前端開發都應該懂的事件循環(event loop)以及異步執行順序(setTimeout、promise和async/await)

JS中的事件循環原理以及異步執行過程這些知識點對新手來說可能有點難,但是是必須邁過的坎,逃避是解決不了問題的,本篇文章旨在幫你徹底搞懂它們。
說明:本篇文章主要是基於瀏覽器環境,Node環境沒有研究過暫時不討論。文章的內容也是博採衆長以及結合自己的理解完成的,相關參考文獻文章末尾也會給出,若有侵權請告知整改,或有描述不正確的也歡迎提醒。文章會有點長,耐心看完哦~

不廢話,讓我們從簡單到複雜,步步深入,去享受知識的盛宴~

1. JS是單線程的

我們都知道JS是單線程執行的(原因:我們不想並行地操作DOM,DOM樹不是線程安全的,如果多線程,那會造成衝突),one thread --> one call stack --> one thing at a time,也就是說,它只有一個執行棧(call stack),在同一時刻,JS引擎只能做一件事(JS在執行時有一個非常重要的特性:run to complete,只要運行就直到完成)。看到 “JS是單線程的”這句話的時候,不知道你有沒有這樣的疑惑:既然JS是單線程的,那麼我在網頁向後端請求數據的時候,我怎麼還可以操作頁面:我可以滾動頁面,我也可以點擊按鈕,這不是跟JS是單線程的衝突嗎?這個問題困擾了我好久,很大的一個原因是:我以爲瀏覽器單單只是由一個JS引擎構成的。如下圖(我以爲的瀏覽器構造,這裏以谷歌瀏覽器chrome爲例):
JS單線程的理解
這裏小說明一下:V8是谷歌瀏覽器的JS執行引擎,在運行JS代碼的時候,是以函數作爲一個個幀(保存當前函數的執行環境)按代碼的執行順序壓入執行棧(call stack)中,棧頂的函數先執行,執行完畢後彈出再執行下一個函數。其中堆是用來存放各種JS對象的。
假設瀏覽器就是上圖的這種結構的話,執行同步代碼是沒什麼問題的,如下代碼1

function foo() {
	bar()
	console.log('foo')
}
function bar() {
	baz()
	console.log('bar')
}
function baz() {
	console.log('baz')
}

foo()

我們定義了foo、bar、baz三個函數,然後調用foo函數,控制檯輸出的結果爲:

baz
bar
foo

執行過程如下:

  1. 一個全局匿名函數最先執行(JS的全局執行入口,之後的例子將忽略),遇到foo函數被調用,將foo函數壓入執行棧。
  2. 執行foo函數,發現foo函數體中調用了bar函數,則將bar函數壓入執行棧。
  3. 執行bar函數,發現bar函數體中調用了baz函數,又將baz函數壓入執行棧。
  4. 執行baz函數,函數體中只有一條語句console.log('baz'),執行,在控制檯打印:baz,然後baz函數執行完畢彈出執行棧。
  5. 此時的棧頂爲bar函數,bar函數體中的baz()語句已經執行完,接着執行下一條語句(console.log('bar')),在控制檯打印:bar,然後bar函數執行完畢彈出執行棧。
  6. 此時的棧頂爲foo函數,foo函數體中的bar()語句已經執行完,接着執行下一條語句(console.log('foo')),在控制檯打印:foo,然後foo函數執行完畢彈出執行棧。
  7. 至此,執行棧爲空,這一輪執行完畢。

還是圖直觀點,以上步驟對應的執行流程圖如下:
執行過程
非動圖:
JS單線程的理解(例子圖流程)
但是,如果我們代碼中有異步事件該怎麼辦?

2. 事件循環(event loop)

我們改變一下代碼1代碼2如下:

function foo() {
	bar()
	console.log('foo')
}
function bar() {
	baz()
	console.log('bar')
}
function baz() { 
	setTimeout(() => {
		console.log('setTimeout: 2s')
	}, 2000)
	console.log('baz') 
}

foo()

其他都不變,就在baz函數中增加了一個setTimeout函數。根據1中的假設,瀏覽器只由一個JS引擎構成的話,那麼所有的代碼必然同步執行(因爲JS執行是單線程的,所以當前棧頂函數不管執行時間需要多久,執行棧中該函數下面的其他函數必須等它執行完彈出後才能執行(這就是代碼被阻塞的意思)),執行到baz函數體中的setTimeout時應該等2秒,在控制檯中輸出setTimeout: 2s,然後再輸出:baz。所以我們期望的輸出順序應該是:setTimeout: 2s -> baz -> bar -> foo這是錯的)。

瀏覽器如果真這樣設計的話,肯定是有問題的!!! 遇到AJAX請求、setTimeout等比較耗時的操作時,我們頁面需要長時間等待,就被阻塞住啥也幹不了,出現了頁面“假死”,這樣絕對不是我們想要的結果。

實際當然並非我以爲的那樣,這裏先重點提醒一下:JS是單線程的,這一點也沒錯,但是瀏覽器中並不僅僅只是由一個JS引擎構成,它還包括其他的一些線程來處理別的事情。如下圖(此圖參考了Philip Roberts的演講:《Help, I’m stuck in an event-loop》(YouTube版),被牆的可以看這:《Help, I’m stuck in an event-loop》(bilibili版),這視頻推薦大家觀看):
ES5瀏覽器模型瀏覽器除了JS引擎(JS執行線程,後面我們只關注JS引擎中的執行棧)以外,還有Web APIs(瀏覽器提供的接口,這是在JS引擎以外的)線程、GUI渲染線程等(如下表)。JS引擎在執行過程中,如果遇到相關的事件(DOM操作、鼠標點擊事件、滾輪事件、AJAX請求、setTimeout等),並不會因此阻塞,它會將這些事件移交給Web APIs線程處理,而自己則接着往下執行。Web APIs(這裏其實有一個event table,用於記錄各種事件)則會按照一定的規則將這些事件放入一個任務隊列(callback queue,也叫 task queue,HTML標準定義中,任務隊列的數據結構其實不是隊列,而是Set(集合),比如,當前執行棧正在執行,即使有一個定時器回調已經在任務隊列中等待,此時發生了一個鼠標點擊事件,那麼該點擊事件回調也會添加到任務隊列中,此後執行棧變爲空,JS引擎是會先取鼠標點擊事件的回調執行,而不是先添加到任務隊列中的定時器回調。即任務隊列中是由一個個集構成的,各個集的執行先後是確定好的,按集的優先級取回調執行,集內是同一類型的回調纔是按照先進先出的隊列模式)中,當JS執行棧中的代碼執行完畢以後,它就會去任務隊列中獲取一個事件回調放入執行棧中執行,然後如此往復,這就是所謂的事件循環機制

線程名 作用
JS引擎線程 也稱爲JS內核,負責處理JavaScript腳本。(例如V8引擎)
①JS引擎線程負責解析JS腳本,運行代碼。
②JS引擎一直等待着任務隊列中的任務的到來,然後加以處理。
③一個Tab頁(renderer進程)中無論什麼時候都只有一個JS線程運行JS程序。
事件觸發線程 歸屬於渲染進程而不是JS引擎,用來控制事件循環
①當JS引擎執行代碼塊如setTimeout時(也可來自瀏覽器內核的其他線程,如鼠標點擊、Ajax異步請求等),會將對應任務添加到事件線程中。
②當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。
注意:由於JS的單線程關係,所以這些待處理隊列中的事件都是排隊等待JS引擎處理,JS引擎空閒時纔會執行。
定時觸發器線程 setInterval和setTimeout所在的線程
①瀏覽器定時計數器並不是由JS引擎計數的。
②JS引擎時單線程的,如果處於阻塞線程狀態就會影響計時的準確,因此,通過單獨的線程來計時並觸發定時。
③計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行。
注意:W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。
異步http請求線程 XMLHttpRequest在連接後通過瀏覽器新開一個線程請求
將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調放入事件隊列中,再由JS引擎執行。
GUI渲染線程 負責渲染瀏覽器界面,包括:
①解析HTML、CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
②重繪(Repaint)以及迴流(Reflow)處理。

這裏讓我們對事件循環先來做個小總結

  1. JS線程負責處理JS代碼,當遇到一些異步操作的時候,則將這些異步事件移交給Web APIs 處理,自己則繼續往下執行。
  2. Web APIs線程將接收到的事件按照一定規則按順序添加到任務隊列中(應該是添加到任務集合中的各個事件隊列中)。
  3. JS線程處理完當前的所有任務以後(執行棧爲空),它會去查看任務隊列中是否有等待被處理的事件,若有,則取出一個事件回調放入執行棧中執行。
  4. 然後不斷循環第3步。

讓我們來看看真正的瀏覽器中執行代碼1是什麼個流程吧!
TIP:這裏的流程示例網站也是Philip Roberts的演講中提到的(是他本人寫的),可以自己去嘗試嘗試:傳送門
代碼1流程動圖
代碼1中並沒有出現異步事件,也就不會調用到Web API線程,所以動圖中的Web API和任務隊列一直爲空。
這次,讓我們運行一下有異步事件的代碼2看看什麼效果:
代碼2執行流程
可以看到,當JS執行棧執行到baz中的setTimeout時,執行棧將該事件推送給Web API處理(Web API開始計時,而不是JS引擎來計時),自己則不被阻塞繼續執行,當JS執行棧爲空時再去任務隊列中獲取事件執行。所以代碼2的正確運行結果打印出來的順序應該是:baz -> bar -> foo -> setTimeout: 2s
細心的小夥伴可能有發現Web API在計時器時間到達後將匿名回調函數添加到任務隊列中了,雖然定時器時間已到,但它目前並不能執行!!!因爲JS的執行棧此時並非空,必須要等到當前執行棧爲空後纔有機會被召回到執行棧執行。由此,我們可以得出一個結論:setTimeout設置的時間其實只是最小延遲時間,而並不是確切的等待時間。(當主線程的任務耗時比較長的時候,等待時間將會變得更長)

相信有了以上的鋪墊之後,你對瀏覽器中JS的執行流程有點感覺了,讓我們趁熱打鐵,進一步探討事件循環和異步吧~

3. 事件循環(進階)與異步

3.1 試試setTimeout(fn, 0)

現在讓我們試試0秒延時的setTimeout執行會如何,按道理來說0秒延遲就是立即執行,那麼控制檯打印結果應該爲:setTimeout: 0s -> foo,事實如此嗎?

function foo() {
    console.log('foo');
}
setTimeout(function() {
    console.log('setTimeout: 0s');
}, 0);

foo();

實際控制檯打印結果的順序爲:foo -> setTimeout: 0s,來看看實際代碼執行的過程:
setTimeout0
可以看到,即使setTimeout的延時設置爲0(實際上最小延時 >=4ms,參考),JS執行棧也將該延時事件發放給Web API處理,Web API再將事件添加到任務隊列中,等JS執行棧爲空時,該延時事件再壓入執行棧中執行。由此我們可以得出一個結論:JS執行棧只要遇到異步函數,則無腦推給Web APIs處理。與許多其他語言不同,JS永不阻塞(也存在一些遺留的意外:如 alert 或者同步 XHR)。 處理 I/O 通常通過事件回調來執行,所以當一個應用正等待一個 IndexedDB 查詢返回或者一個 XHR 請求返回時,它仍然可以處理其它事情,比如用戶輸入。

3.2 事件循環中的Promise

現在是時候再深入一步了,我們ES6中新增的promise已經迫不及待地亮相了!(本篇文章不討論Promise相關的知識點,如果你對Promise不瞭解的話,建議先去看看相關知識點)。
其實以上的瀏覽器模型是ES5標準的,ES6+標準中的任務隊列在此基礎上新增了一種,變成了如下兩種:

  1. 宏任務隊列(大家稱之爲macrotask queue,即callback queue):按HTML標準嚴格來說,其實沒有macrotask queue這種說法,它也就是ES5中的事件隊列,該隊列存放的是:DOM事件、AJAX事件、setTimeout事件等的回調。可以通過setTimeout(func)即可將func函數添加到宏任務隊列中(使用場景:將計算耗時長的任務切分成小塊,以便於瀏覽器有空處理用戶事件,以及顯示耗時進度)。
  2. 微任務隊列(microtask queue):存放的是Promise事件、nextTick事件(Node.js)等。有一個特殊的函數queueMicrotask(func)可以將func函數添加到微任務隊列中。

那麼,現在的事件循環模型就變成了如下的樣子:
ES6瀏覽器模型
事件循環的處理流程變成了如下:

  1. JS線程負責處理JS代碼,當遇到一些異步操作的時候,則將這些異步事件移交給Web APIs 處理,自己則繼續往下執行。
  2. Web APIs線程將接收到的事件按照一定規則添加到任務隊列中,宏事件(DOM事件、Ajax事件、setTimeout事件等)添加到宏任務隊列中,微事件(Promise、nextTick)添加到微事件隊列中
  3. JS線程處理完當前的所有任務以後(執行棧爲空),它會先去微任務隊列獲取事件,並將微任務隊列中的所有事件一件件執行完畢,直到微任務隊列爲空後再去宏任務隊列中取出一個事件執行(每次取完一個宏任務隊列中的事件執行完畢後,都先檢查微任務隊列)。
  4. 然後不斷循環第3步。

一圖勝千言,畫個流程圖更加清晰,幫助記憶:
ES6事件循環
排一下先後順序: 執行棧 --> 微任務 --> 渲染 --> 下一個宏任務

3.2.1 單獨使用Promise

先來個只有Promise的例子熱熱身:

function foo() {
	console.log('foo')
}

console.log('global start')

new Promise((resolve) => {
	console.log('promise')
	resolve()
}).then(() => {
	console.log('promise then')
})

foo()

console.log('global end')

控制檯輸出的結果爲:

//前面的序號不用管,是給接下來的描述用的
global start
promise
foo
global end
promise then

代碼執行的解釋:

  1. 執行console.log('global start')語句,打印出:global start
  2. 繼續往下執行,遇到new Promise(....),執行之(這裏說明一點:在使用new關鍵字來創建Promise對象時,傳遞給Promise的函數稱爲executor,當promise被創建的時候executor函數會自動執行,而then裏面的東西纔是異步執行的部分),Promise參數中的匿名函數與主線程同步執行,執行console.log('promise')打印出:promise。在執行resolve()之後Promise狀態變爲resolved,再繼續執行then(...),遇到then則將其提交給Web API處理,Web API將其添加到微任務隊列(注意:此時微任務隊列中已有一個Promise事件待處理)。
  3. 執行棧在轉交完Promise事件後,繼續往下執行,到達語句foo(),執行foo函數,打印出:foo
  4. 執行棧繼續執行,到達語句console.log('global end'),執行後打印出:global end至此,本輪事件循環已結束,執行棧爲空
  5. 事件循環機制首先查看微任務隊列是否爲空,發現有一個Promise事件待執行,則將其壓入執行棧,執行then中的代碼,執行console.log('promise then'),打印出:promise then至此,新的一輪事件循環(Promise事件)已結束,執行棧爲空。(注意:此時微任務隊列爲空
  6. 執行棧變空後又先查看微任務隊列,發現微任務隊列已爲空,然後再查看宏任務隊列,發現宏任務隊列也爲空,那麼執行棧進入等待事件狀態。

用動圖來展示一下執行的流程(備註:該demo網站並未畫出微任務隊列,我們需自己腦補一下microtask queue):
promise1

3.2.2 Promise結合setTimeout

我們已經對單獨的宏任務和微任務的執行流程分別做了分析,現在讓我們混合這兩種任務的事件來看看結果如何,來個代碼示例小試牛刀:

function foo() {
	console.log('foo')
}

console.log('global start')

setTimeout(() => {
	console.log('setTimeout: 0s')
}, 0)

new Promise((resolve) => {
	console.log('promise')
	resolve()
}).then(() => {
	console.log('promise then')
})

foo()

console.log('global end')

控制檯輸出的結果爲:

global start
promise
foo
global end
promise then
setTimeout: 0S

代碼執行的解釋(相比3.2.1中的代碼,黃色背景爲改變的部分):

  1. 執行console.log('global start')語句,打印出:global start
  2. 繼續往下執行,遇到setTimeout,JS執行棧將其移交給Web API處理。 延遲0秒後,Web API將setTimeout事件添加到宏任務隊列注意:此時宏任務隊列中已有一個setTimeout事件待處理)。
  3. JS線程轉交setTimeout事件後自己則繼續往下執行,遇到new Promise(....),執行之,Promise參數中的匿名函數同步執行,執行console.log('promise')打印出:promise。在執行resolve()之後Promise狀態變爲resolved,再繼續執行then(...),遇到then則將其提交給Web API處理,Web API將其添加到微任務隊列(注意:此時微任務隊列中已有一個Promise事件待處理)。
  4. 執行棧在轉交完Promise事件後,繼續往下執行,到達語句foo(),執行foo函數,打印出foo
  5. 執行棧繼續執行,到達語句console.log('global end'),執行後打印出:global end。至此,本輪事件循環已結束,執行棧爲空。
  6. 事件循環機制首先查看微任務隊列是否爲空,發現有一個Promise事件待執行,則將其壓入執行棧,執行then中的代碼,執行console.log('promise then'),打印出:promise then。至此,新的一輪事件循環(Promise事件)已結束,執行棧爲空。(注意:此時微任務隊列爲空
  7. 執行棧變空後又先查看微任務隊列,發現微任務隊列已爲空,然後再查看宏任務隊列,發現有一setTimeout事件待處理,則將setTimeout中的匿名函數壓入執行棧中執行,執行console.log('setTimeout: 0s')語句,打印出:setTimeout: 0s。至此,新的一輪事件循環(setTimeout事件)已結束,執行棧爲空。注意:此時微任務隊列爲空,宏任務隊列也爲空
  8. 執行棧變空後又先查看微任務隊列,發現微任務隊列已爲空,然後再查看宏任務隊列,發現宏任務隊列也爲空,那麼執行棧進入等待事件狀態。

這個例子比較詳細地解釋了一遍,一共發生了三次事件循環。同理,還是用個動圖來直觀地展示代碼執行過程吧!
promise2
相信耐心看到這的你已經對事件循環機制以及宏任務和微任務的執行順序有個清晰的瞭解了吧!不過,還沒結束哦,我們async/await(不瞭解的人建議先去補習一下:async_function)還沒講呢!

3.3 事件循環中的async/await

這裏簡單介紹下async函數:

  • 函數前面async關鍵字的作用就2點:①這個函數總是返回一個promise。②允許函數內使用await關鍵字。
  • 關鍵字await使async函數一直等待(執行棧當然不可能停下來等待的,await將其後面的內容包裝成promise交給Web APIs後,執行棧會跳出async函數繼續執行),直到promise執行完並返回結果。await只在async函數函數裏面奏效。
  • async函數只是一種比promise更優雅得獲取promise結果(promise鏈式調用時)的一種語法而已。

像上面一樣,我們先單獨拎出async函數來看看是怎麼樣個執行流程吧~

function foo() {
	console.log('foo')
}

async function async1() {
	console.log('async1 start')
	await async2()
	console.log('async1 end')
}

async function async2() {
	console.log('async2')
}

console.log('global start')
async1()
foo()
console.log('global end')

這裏就增加了兩個async函數:async1、async2。執行的結果如下:

global start
async1 start
async2
foo
global end
async1 end

我們再來逐條解析一下代碼的執行過程吧(前面那些我們已經懂的就不重複了):

  1. 首先執行console.log('global start'),打印出:global start
  2. 執行async1(),進入到async1函數體內,執行console.log('async1 start'),打印出:async1 start。接着執行await async2(),這裏await關鍵字的作用就是await下面的代碼只有當await後面的promise返回結果後纔可以執行(此時,微任務隊列有一事件,其實就是Promise事件),而await async2()語句就像執行普通函數一樣執行async2(),進入到async2函數體中;執行console.log('async2'),打印出:async2。async2函數執行結束彈出執行棧。
  3. 因爲await關鍵字之後的語句已經被暫停,那麼async1函數執行結束,彈出執行棧。JS主線程繼續向下執行,執行foo()函數打印出:foo
  4. 執行console.log('global end'),打印出:global end該語句之後再無其他需執行的代碼,執行棧爲空,則本輪事件執行結束
  5. 此時,事件循環機制開始工作:同理,先查看微任務隊列,執行完所有已存在的微任務事件後再去查看宏任務隊列。目前微任務隊列中的事件即爲async1函數中await async2()語句,async2函數執行完畢後,promise狀態變爲settled,之後的代碼就可以繼續執行了(可以這麼理解:用一個匿名函數包裹await語句之後的代碼作爲一個微任務事件),執行console.log('async1 end')語句,打印出:async1 end。執行棧又爲空,本輪事件也執行結束。
  6. 事件循環機制再查看微任務隊列,發現爲空,再去查看宏任務隊列,發現也爲空,則進入等待事件狀態。

至此,單一事件類型我們都掌握了,下面我們綜合演練一下!

4. 大綜合(自測)

這裏來幾道常見的題目來考察自己的掌握程度以及進一步鞏固吧!這裏不再逐步分析了,有困惑的可以留言再解答。

4.1 簡單融合

//請寫出輸出內容
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
	console.log('async2');
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');

輸出結果:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

4.2 變形1

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
	    console.log('promise1');
	    resolve();
	}).then(function() {
	    console.log('promise2');
    });
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');

輸出的結果:

script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

4.3 變形2

async function async1() {
    console.log('async1 start');
    await async2();
    //更改如下:
    setTimeout(function() {
        console.log('setTimeout1')
    },0)
}
async function async2() {
    //更改如下:
	setTimeout(function() {
		console.log('setTimeout2')
	},0)
}

console.log('script start');

setTimeout(function() {
    console.log('setTimeout3');
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');

輸出的結果:

script start
async1 start
promise1
script end
promise2
setTimeout3
setTimeout2
setTimeout1

4.4 變形3

async function a1 () {
    console.log('a1 start')
    await a2()
    console.log('a1 end')
}
async function a2 () {
    console.log('a2')
}

console.log('script start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
    console.log('promise1')
})

a1()

let promise2 = new Promise((resolve) => {
    resolve('promise2.then')
    console.log('promise2')
})

promise2.then((res) => {
    console.log(res)
    Promise.resolve().then(() => {
        console.log('promise3')
    })
})
console.log('script end')

輸出的結果:

script start
a1 start
a2
promise2
script end
promise1
a1 end
promise2.then
promise3
setTimeout

怎麼樣,都做對了嗎?其實就是將這幾個異步事件糅合在一起罷了,只要我們分別掌握了它們的執行過程,一步步拆開分析,一點都不難,這都是紙老虎而已!

5. 結語

呼~長吁一口氣,相信看到這的你已經疲憊了,不過恭喜:你應該完全掌握了事件循環以及異步執行機制了吧!最後,讓我們再總結一下本文涉及的要點吧!

  1. JS是單線程執行的,同一時間只能處理一件事。但是瀏覽器是有多個線程的,JS引擎通過分發這些耗時的異步事件(AJAX請求、DOM操作等)給Wep APIs線程處理,因此避免了單線程被耗時的異步事件阻塞的問題。

  2. Web APIs線程會將接收到的所有事件中已完成的事件根據類別分別將它們添加到相應的任務隊列中。其中任務隊列分以下兩種:

    • 宏任務隊列(macrotask queue):其實是叫任務隊列,ES5稱task queue,也即本文圖中的callback queue,macrotask是我們給它的別名,原因只是爲了與ES6新增的microtask隊列作區分而這樣稱呼,HTML標準中並沒有macrotask這種說法。它存放的是DOM事件、AJAX事件、setTimeout事件等。
    • 微任務隊列(microtask queue):它存放的是Promise事件、nextTick事件等。優先級比macrotask高。
  3. 事件循環(event loop) 機制是爲了協調事件(events)、用戶交互(user interaction)、JS腳本(scripts)、頁面渲染(rendering)、網絡請求(networking)等等事件的有序執行而設置(定義由HTML標準給出,實現方式是靠各個瀏覽器廠商自己實現)。事件循環的過程如下:

    • JS引擎執行一個事件,當遇到異步事件時則將其交給瀏覽器的Web APIs線程處理,然後該事件繼續執行,永遠不會被搶佔,一直執行到該事件結束爲止(run to complete)。
    • 當JS引擎執行完當前事件(即執行棧變爲空)之後,它會先去查看microtask隊列,將microtask隊列中的所有待執行事件全部執行完畢。
    • 等微任務事件全部執行完畢後,再進行頁面的渲染,此時表明一輪事件循環的過程結束。然後再去查看macrotask隊列,取出一個宏事件添加到執行棧執行,開始一輪新的事件,執行完畢後再去執行所有微任務事件…如此往復。此即事件循環的執行過程。

    打個比方幫助理解宏任務事件就像是普通用戶,而微任務事件就像是VIP用戶,執行棧要先把所有在等待的VIP用戶服務好了以後才能給在等待的普通用戶服務,而且每次服務完一個普通用戶以後都要先看看有沒有VIP用戶在等待,若有,則VIP用戶優先(PS:人民幣玩家真的可以爲所欲爲,hah…)。當然,執行棧正在給一個普通用戶服務的時候,這時即使來了VIP用戶,他也是需要等待執行棧服務完該普通用戶後才能輪到他。

  4. setTimeout設置的時間其實只是最小延遲時間,並不是確切的等待時間。實際上最小延時 >=4ms,小於4ms的會被當做4ms。

  5. promise 對象是由關鍵字 new 及Promise構造函數來創建的。該構造函數會把一個叫做“處理器函數”(executor function)的函數作爲它的參數(即 new Promise(...)中的...的內容)。這個“處理器函數”是在promise創建時是自動執行的,.then之後的內容纔是異步內容,會交給Web APIs處理,然後被添加到微任務隊列

  6. async/awaitasync函數其實是Generator函數的語法糖(解釋一下“語法糖”:就是添加標準以外的語法以方便開發人員使用,本質上還是基於已有標準提供的語法進行封裝來實現的),async function 聲明用於定義一個返回 AsyncFunction 對象的異步函數。執行async函數時,遇到await關鍵字時,await 語句產生一個promise,await 語句之後的代碼被暫停執行,等promise有結果(狀態變爲settled)以後再接着執行。

呼~ 結束了,休息一下~

若對你有幫助,可以支持一下作者創作更多好文章哦~
一分錢也是愛~
讚賞碼

參考文獻:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章