面對 Promise 的思考路徑

Promise 是常見的面試問題,相信各位打工仔肯定是不陌生,但往往會是:用的時候覺得理所當然,成為考題時卻覺得絆手絆腳難以解釋。這邊我想用一篇文章來整理思考路徑,以讓下次碰到這類提問是有個思考模型。

前言(廢話)

JavaScript 是一個非同步(異步)的語言,我們常常在解決使用者觸發的事件、Network IO 的請求、計時器觸發…之間打轉,對比其他同步語言,可以直落落的寫下來,JavaScript 仰賴的是回調函數(Callback)的幫助來分工合作。

心智模型

學習、記憶、思考總是需要有一個如同樹圖的聯想方式,在這個章節我們先站在面試官的角度思考一下:如果你要生出 Promise 的題目來考面試者,該怎麼下手呢?

  • 決定 Promise tasks 競賽方式的: Promise.all()Promise.allSettled()Promise.any()Promise.race()
  • 決定 Promise task(s) 後續的: Promise.then()Promise.catch()Promise.finally()

決定 Promise 狀態(結果) — resolve & reject

當我們在 new 一個 Promise 實例的時候,Promise 會給我們兩個決定結果的函數,大致上如下:

const SimplePromise = new Promise((resolve,reject)=>{
if(somethingGood) resolve(result);
if(somethingBad) reject(new Error('something bad'));
})
SimplePromise.then().catch()
> Promise.resolve(true);
< Promise {<fulfilled>: true}
> Promise.reject(false);
< Promise {<rejected>: false}
X VM581:1 Uncaught (in promise) false (anonymous) @ VM581:1

決定 Promise tasks 競賽方式— all 、 allSettled 、 race 、 any

當我們有多個 Promise 綁定在一起處理的時候,有兩種大宗的 scenario:

  1. Asynchronous (去做各自的事)
promiseA
.then(result=>PromiseBFactory(result))
.then(finalResult=>console.log(finalResult))
raceRule([Promise1,Promise2.Promise3]).then(???)
  1. 大家一起結束 Promise.all 、 Promise.allSettled
  • 只有 fulfilled 才算結束 Promise.any (一個 reject 等下一個,全部 reject 進入 rejected)
  • 只有 fulfilled 才算結束 Promise.all (其中一個 rejected 的話會進入 catch)
假設現在有兩對情侶 A B,他們在大學校園的樹下埋下六人的合照,交往過程是一個 Promise pending , 最終結婚是 fulfill ,而分手則是是 rejected。他們約定好在以下的狀況下拆開時光膠囊:1. 如果其中有一對結婚了,拆時光膠囊吧! Promise.any([A,B])
2. 如果其中有人結婚或分手了,丟掉時光膠囊吧! Promise.race([A,B])
3. 如果大家都結婚了,拆時光膠囊吧! Promise.all([A,B])
4. 如果大家狀態確定了(分手、結婚),丟掉時光膠囊吧 Promise.allSettled([A,B])
最終情侶A在4年後結婚,情侶B則是在三年後就分手了,那麼以上四個case會是怎麼樣呢?
//1. 如果其中有一對結婚了,拆時光膠囊吧! Promise.any([A,B])
const A = new Promise(resolve=>setTimeout(resolve,4,'A'));
const B = new Promise((_,reject)=>setTimeout(reject,3,'B'));
setTimeout(()=>Promise.any([A,B]).then(console.log));
// A
//2. 如果其中有人結婚或分手了,丟掉時光膠囊吧! Promise.race([A,B])
const A = new Promise(resolve=>setTimeout(resolve,4,'A'));
const B = new Promise((_,reject)=>setTimeout(reject,3,'B'));
setTimeout(()=>Promise.race([A,B]).then(console.log)).catch(console.error); // Error: B// B如果在五年後差分手,那麼不會走到 catch 喔!
//3. 如果大家都結婚了,拆時光膠囊吧! Promise.all([A,B])
const A = new Promise(resolve=>setTimeout(resolve,4,'A'));
const B = new Promise((_,reject)=>setTimeout(reject,3,'B'));
setTimeout(()=>Promise.all([A,B])
.then(console.log)
.catch(console.error)); // B
// B is the reject reason
// 儘管B在五年後差分手 一樣是導致all method失敗的reason喔!
// 4. 如果大家狀態確定了(分手、結婚),丟掉時光膠囊吧 Promise.allSettled([A,B])
const A = new Promise(resolve=>setTimeout(resolve,4,'A'));
const B = new Promise((_,reject)=>setTimeout(reject,3,'B'));
setTimeout(()=>Promise.allSettled([A,B]).then(console.log));
// [
// { status: 'fulfilled', value: 'A' },
// { status: 'rejected', reason: 'B' }
// ]
  • 全體完成為主的 all & allSettled
  • 只有成功才算的 any & all

決定競賽結果的後續回調的 — then、catch、finally

  • Promise 走到 fulfilled 狀態, result 傳進去 then 的 callback
  • Promise 走到 rejected 狀態, reason 傳進去 catch 的 callback
  • Promise 不論走到哪個狀態,當 then 與 catch 執行 完,接著執行 finally 的 callback
// 1. Promise fulfilled
Promise.resolve("I'm result").then(console.log) // I'm result
// 2. Promise rejected
Promise.reject("reject reason").catch(console.error) // reject reason
// 3. Promise finally
const APromise = new Promise((resolve,reject)=>{
// I don't know wtf will going on
})
APromise
.then(result=>{
// Processing if APromise fulfilled
})
.catch(reason=>{
// Processing if APromise rejected
})
.finally(()=>{
// MUST DO THIS WHENEVER FULFILLED OR REJECTED
})

尾聲

透過這樣的分類方式,我們可以輕鬆的在任意的 Promise 實作考題中如魚得水(大概吧?),無論你是想從中抽考面試候選者,或是你自己在面對面試題的時候,都能輕易從 Promise 的既有方法中,找到題目想要引導的思路與本意。

1. 由於我們在UI測試實驗中,發現使用者只願意等待2~5秒的時間在請求加載上,能否為我們設計、封裝一回傳 Promise 的高階函數 foolProofRequest ,其接受一網路請求的函數 fn 作為第一參數,一個毫秒數 n 為第二參數,其會去做非同步的網路請求,但是 n 毫秒沒有回傳就 timeout 的函數。const foolProofRequest = (fn, n) => ?
2. 接續上題目,由於我們非常注重使用者交互,我們準備了三個同樣回傳格式的 endpoints A B C,能否將其設定為 A 容忍秒數為 2、B 容忍秒數為 3、C 容忍秒數為 1,且當一 endpoint 得到了回傳結果後,就將其傳入渲染函數 render 裡頭,當三個 endpoint 都失效、過期,請呼叫 exceptionHandler 函數,且在全部行為結束後,呼叫 Helloworld 函數。注1:若 A endpoint 例外或是超過兩秒,我們還是會等待 B endpoint 完成。
注2:若 A B C endpoint 皆例外或是過期,我們
Promise.???([foolProofRequest(A,2000),foolProofRequest(B,3000),foolProofRequest(C,1000)])
.then(???)
.catch(???)
.finally(???)

If any interest, 👉 https://realdennis.me.

If any interest, 👉 https://realdennis.me.