實作promise.all理解promise與microtask
之前有寫一篇文章在討論 promise 的使用,所以之前提到的這邊就不多講了,現在將帶大家進一步窺探 promise ~
實作 Promise.all
Let’s step by step!
Promise.all 的運作方式是,全部的 promise 都 resolve 則通過。
所以我們可以得出一個簡單的公式:
裡面全部的 promise 都 resolve,則這個 promise 就會 resolve
所以我在自己設計的 promiseAll,裡面會 return 一個 promise
大概的架構會長這樣:
1 | const promiseAll = (promises) => { |
我需要把所有已經被 resolve 的 response 累積起來,以及計數被 resolve 的 promise,所以會這樣設計:
1 | const promiseAll = (promises) => { |
再來我們需要把所有的 promise 一起執行
這邊請注意,你要做的是 concurence 而不是 sequence
如果做成序列化的話,就失去意義了,所以這邊要做的是把所有 promise 並行執行:
1 | const promiseAll = (promises) => { |
再來請思考一下 Promise.all 的特性,只要有一個 reject 就會整個 reject
所以在跑 promise 的時候,只要 reject,就使用最外層的 reject:
1 | const promiseAll = (promises) => { |
Promise.all 會回傳一個 response array,所以我們直接 push 到 resolvedPromiseResponses
並且把 counter 變數 numOfSolvedPromises + 1:
1 | const promiseAll = (promises) => { |
最後,在裡面加上一個判斷式讓最後一個完成的 promise 執行最外層的 resolve
只要最後一個任務被 resolve,整個 promises 就算是 resolve 了:
1 | const promiseAll = (promises) => { |
測試實作的 Promise
resolve 的例子
這邊實作簡單的 pending function 來測試:
1 | const sleep = (time) => { |
開始測試,最後會回傳一個被 resolve 的 response array:
1 | const promises = [sleep(2000), sleep(3000), sleep(1000)]; |
輸出:
[1000, 2000, 3000]
reject 的例子
測試 reject 的例子只要把某一個 case reject 就能測試囉:
1 | const sleep = (time) => { |
一樣的程式測試,理論上會回傳 I am wake up early!! :
1 | const promises = [sleep(2000), sleep(3000), sleep(1000)]; |
如果是實作 Promise.allsettled 呢
Promise.allsettled 的特性是,不管裡面的 promise 如何,這個 Promise 一定會被 resolve
所以我們可以把上面 catch 裏面的程式稍微調整一下:
1 | const promiseAllSettled = (promises) => { |
這樣輸出就會包含所有結果了:
[1000, “I am wake up early!!”, 3000]
針對 Promise 的添加方法
要在 Promise 添加方法,你有這兩種選擇:
- instance method
- static method / class method
這兩種我都來介紹怎麼使用,最後我們再來評估該選哪個
添加 instance method
JavaScript 是 prototype based 的程式語言,比起其他程式語言特別的地方是,他們具有 function 這個型別
我們會稱呼他為原型物件,但是我們還是會直接稱它為物件
每個原型物件也會有自己的原型而形成原型鏈,最後直到接到 null 為止
當我們要在特定的原型添加屬性或是函數的話,只需要直接呼叫 prototype 來實作即可:
1 | function Example() { |
如此一來,基於原型實現的物件就可以擁有這些原型方法可以用了:
1 | const a = new Example(); |
static method
新增靜態方法的話,不用產生實體就可以使用方法
而新增的方法很簡單,只需要直接設置即可:
1 | function Example() { |
直接使用:
1 | Example.myFunction(); |
開始評估與添加
基於以上知識之後,我們再來看看實際的 Promise.all
他是直接使用靜態方法來使用的,所以我們的選擇就會是 static method
所以我們可以這麼做:
1 | Promise.promiseAll = (promises) => { |
測試跟之前一樣,稍微改一下:
1 | const sleep = (time) => { |
promiseAllSettled 的實作方法也差不多,輪到你試試看吧
補充一些知識
JavaScript 是單執行緒的程式語言,在 browser 上運行則是會以主執行緒來執行
JS 在執行的 Priority 為:Sync > Nanotask > Microtask > Task / Macrotask
如果要像多執行緒一樣能夠非同步執行程式得仰賴 event loop,JS 基於以上的優先順序執行任務來執行非同步
如果時常開發 Node.js 的人,可能比較會知道這方面的知識與效能議題
Macro task
當我們在前端討論 Macro task 的時候,通常都是在講 Web API 與 V8 engine 的互動
V8 engine 擁有的 call stack 執行到 Macro task 時,就會搭配 Macro task queue 跑 event loop
我們先拿最小的 Macro task 來看,常見的 setTimeout, setInterval, I/O… 皆屬於這個
當 engine 處於忙碌狀態的時候,會把任務放到 task queue 裏面,再來依序執行
在執行的過程不會 trigger render,如果 task 執行太久也會導致任務被 block
例如:alert 被呼叫了,但是如果不關閉 alert 就不會執行接下來的 task 了
Micro task
Micro task 包含:Promise, process.nextTick, queueMicrotask…
當我們希望以非同步的方式執行同步的時候就會需要用到它們
只是在瀏覽器上,我們大部分都是使用 Promise,其他比較會在 Node.js 上看到
這也是為什麼我們在討論前端 event loop 時,直接以 Macro task 的當例子討論了
Promise 時常使用 then / catch / finally 處理程式
儘管 Promise 被 resolve,依然會繼續執行 then / catch / finally 內的程式
以下面的例子來說,你覺得順序是什麼:
1 | // 1 |
1 是 Macrotask,所以 Web API 執行完會丟到 Macrotask queue。
2 的 Promise 始於 Microtask,會把 then 丟入 Microtask queue。
3 會先優先被執行,再來剛剛提到 Microtask 會優先被執行,所以顯示順序會是:outer, Promise, setTimeout
參考資料
Event loop: microtasks and macrotasks
Microtasks
JS 原力覺醒 Day15 - Macrotask 與 MicroTask