JS 非同步執行順序 -- 從一道面試題說起

安可心_1024發表於2020-11-28

這道題可以說是面試必考了,我在筆試中就遇到過好多次,你們應該都遇到過吧?。。以前拿到這道題時,我整個人都是懵的,看著程式碼就覺得又長又繞的,最後總是不能完全做對。

  1. 題目
  2. 解題步驟
  3. 總結

1.題目

請輸出下面的執行結果:

	new Promise(resolve => {
		setTimeout(() => {console.log(0)}, 0)
		resolve()
	}).then(() => {
		setTimeout(() => {console.log(1)}, 0)
	})

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

	new Promise(resolve => {
		setTimeout(resolve, 0)
	}).then(() => {
		console.log(3)
		setTimeout(() => {console.log(4)}, 0)
		new Promise(r => r()).then(() => {console.log(5)})
	})
	
	setTimeout(() => {console.log(6)}, 0)
	
	new Promise(resolve => {
		console.log(7)
		resolve()
	}).then(() => {
		console.log(8)
	})

	for(var i = 9; i < 12; i++) { 
		setTimeout(() => {console.log(i)}, 0)
		console.log(i)
	}

可以先自己想想奧~

先講原理或結論可能會讓你們看得比較枯燥,所以我決定先講我的解題步驟;然後再去總結 JS 非同步執行過程中的一些規則和結論。

結合圖片來講會比較清楚,下面使用到的圖片也都是我自己畫的。

想起了之前耐心給我講解過這個題的面試官,他是在紙上邊畫邊給我講的,講完的那一瞬間我感覺真的懂了(大部分),原本懵C的我,看到他畫的圖後思路清晰了不少。

還有,像理解原型、原型鏈啊這些相對比較抽象的概念也可以試試畫圖,自己沒事就多畫幾遍,隔段時間忘了就再畫幾遍,對於我這種笨笨的人是真的挺有效的。看了再多的文章真的不如自己動手做一遍!!就是這個道理昂~

有點囉嗦了叭,那麼下面進入正題了!!

2.解題步驟

非同步任務裡面有兩種:巨集任務、微任務。下面為了描述方便,我將巨集任務簡稱為 H,微任務簡稱為 W。

2.1 思路:

圖1

圖1

注:看每一步的時候,最好結合前面的圖片一起對比看奧~

執行順序如下:

①如上圖1,JS 按照從上到下的順序執行,執行過程中遇到非同步任務(包括巨集任務和微任務),都會先被加入到任務佇列中,再繼續向下執行同步程式碼。

這一步中巨集任務 H1-H7 加入到巨集任務佇列,微任務 W1-W2 加入到微任務佇列。

這裡要注意,Promise 是會立即執行的,Promise 在建立的時候就會執行,所以 7 會被直接列印出來。

這一步列印: 7 9 10 11

此時,同步程式碼執行完。

圖2

圖2

②如上圖2,執行微任務 W1。會遇到巨集任務 H8,它被新增到巨集任務佇列末尾。

③執行微任務 W2。這一步列印: 8

這一步執行完後,當前的微任務佇列就清空了,下面會開始執行巨集任務。

④執行巨集任務 H1。這一步列印: 0

這一步沒有產生其它的微任務,所以會繼續向下執行其它的巨集任務。

⑤執行巨集任務 H2,這一步列印: 2

圖3

圖3

⑥如上圖3,執行巨集任務 H3。微任務 W3 被推入微任務列表。

之前,H3 還未執行時,Promise 中的 resolve 函式也未執行,Promise 處於 pending 狀態,所以 then 中的函式是不會被推到微任務佇列中的;

而 H3 執行後,resolve 函式也會執行,Promise 的狀態由 pending 變為 fulfilled,then 才會被 Promise 呼叫。

注意:

1.只有 Promise 呼叫了 then 的時候,then 中的函式才會被推入到微任務佇列中;
2.而 Promise 呼叫 then 的前提是 Promise 的狀態為 fullfilled

Promise 這一塊不清楚的話,可以去阮一峰的 ES6 中補一下奧~

圖4

圖4

⑦如上圖,執行微任務 W3。這一步列印: 3

巨集任務 H9 和 微任務 W4 分別加入到巨集任務佇列和微任務佇列。

⑧執行微任務 W4,這一步列印: 5

同樣地,和 ③ 一樣,這一步執行完後,當前的微任務佇列就清空了,下面會開始執行巨集任務。

注意:

1.當前微任務佇列清空後才會繼續執行巨集任務;
2.而巨集任務是每執行一個,就去看下微任務佇列是否有內容,有的話就挨個執行微任務,直到將微任務佇列清空,才會再執行下一個巨集任務。

⑨執行巨集任務 H4,這一步列印: 6

⑩執行巨集任務 H5 H6 H7,這一步列印: 12 12 12
這裡一定要注意!!容易搞錯。for 迴圈了 3 次,會產生 3 個巨集任務。

⑪執行巨集任務 H8,這一步列印: 1

⑫執行巨集任務 H9,這一步列印: 4

哇~到此,就執行完了!!

2.2 答案:
所以最後執行結果為:7 9 10 11 8 0 2 3 5 6 12 12 12 1 4

3.總結

3.1 為什麼需要非同步

  • JS 是單執行緒的,同一時間只能做一件事
  • JS 和 DOM 渲染共用一個執行緒,因為 JS 可修改 DOM 結構,當 JS 執行時 DOM 渲染要停止,DOM 渲染時 JS 也要停止
  • 遇到等待(如網路請求,定時任務等),不應該被卡住
  • 同步會阻塞程式碼執行,而非同步不會阻塞程式碼執行

3.2 關於 Promise 【重要】

  1. Promise 解決了什麼問題
    主要解決了回撥地獄的問題。
  2. 三種狀態
    pending(進行中)、fulfilled(已成功)和 rejected(已失敗)
  3. 狀態變化
    只有兩種情況:
    ①pending -> fulfilled(成功了)
    ②pending -> rejected(失敗了)
    還要注意:變化不可逆!!
  4. 狀態的表現【重要重要】
    ①pending 狀態,不會觸發 then 和 catch
    ②fulfilled 狀態,會觸發後續的 then 回撥函式
    ③rejected 狀態,會觸發後續的 catch 回撥函式
  5. then 和 catch 對狀態的影響
    then 正常返回 fulfilled,裡面有報錯則返回 rejected ;
    catch 正常返回 fulfilled(注意!!),裡面有報錯則返回 rejected。

這裡對 Promise 只是做了一個簡單的總結,詳細的我之後打算再專門寫一篇文章(先挖個坑啦)。

3.3 JS 執行順序【簡單版】

  1. 按照從上到下的順序,一行一行執行
  2. 如果某一行執行報錯,則停止下面程式碼的執行
  3. 先把同步程式碼執行完,再執行非同步程式碼

3.4 JS 執行順序【加上 Event Loop】

  1. 同步程式碼會一行一行放在 Call Stack 中執行
  2. 遇到非同步,會先記錄下,等待時機(定時器、網路請求等)
  3. 時機到了,就移動到 Callback Queue 中
  4. 如果 Call Stack 為空(即同步程式碼執行完),Event Loop 開始工作
  5. 輪詢查詢 Callback Queue,如果其中有內容,則移到 Call Stack 中執行

如下圖(圖5):簡單畫了個圖…畫的不太好,別見怪~~

圖5

圖5

3.5 JS 執行順序【加上微任務、巨集任務】

  1. 同步程式碼會一行一行放在 Call Stack 中執行
  2. 遇到非同步,會先記錄下,等待時機(定時器、網路請求等)
  3. 時機到了,就移到佇列中
    • 巨集任務會經過 Web API 後,再移到巨集任務佇列中。
      如:執行程式碼時遇到 setTimeout,會先將它扔給 Web API 中的 timer,當定時時間到了(即時機到了)之後,再被推到巨集任務佇列中。
    • 微任務不會經過 Web API,會直接移動到微任務佇列 micro task queue 中。
      如:執行程式碼時遇到了 Promise,會將 then 中內容移到微任務佇列中。
  4. 如果 Call Stack 為空(即同步程式碼執行完),Event Loop 開始工作
  5. 先去查詢微任務佇列,如果有內容,則推到 Call Stack 中執行
  6. 當微任務佇列清空後,再去查詢巨集任務佇列,如果有內容,則推到 Call Stack 中執行
  7. 如上步驟迴圈

如下圖(圖6):

圖6

圖6

3.6 微任務 & 巨集任務

  1. 常見的巨集任務、微任務分別有哪些
    巨集任務:setTimeout、setInterval、Ajax、DOM事件
    微任務:Promise.then()、async/await
  2. 微任務和巨集任務的區別
    ①巨集任務:DOM 渲染後觸發
    ②微任務:DOM 渲染前觸發
    ③微任務執行時機比巨集任務要早
    注意:
    微任務執行時會放到一個單獨的 micro task queue(微任務佇列)中,和巨集任務佇列是分開的。
    原因:微任務是 ES6 語法規定的,巨集任務是由瀏覽器規定的
  3. 巨集任務、微任務和 DOM 渲染的關係
    • Call Stack 清空
    • 執行當前的微任務
    • 嘗試 DOM 渲染
    • 執行巨集任務

以上,如有不正確或描述不準確的地方歡迎評論留下意見。一起加油鴨~

相關文章