[JS筆記] 避免導致Callback Hell的Promise

Kim.H
7 min readMay 7, 2023

--

先前有寫了一篇介紹何謂callback function的文章,提到當程式流程較複雜時,如使用callback來處理非同步任務,容易導致callback hell發生,使程式碼不易維護、閱讀,這篇文章就要來討論處理非同步時,改善callback難以閱讀的下一代方法-Promise

同步 V.S 非同步

Javascript是一個單執行續(單線程)的語言,這裡可以理解成「JS一次只能做一件事」,例如巷口的麵店只有老闆一人作業,同一時間他只能做一件事,如果同時有客人要點餐、等餐、結帳,老闆就需要做完其中一件之後才能去做下一件事。

同步的概念,就是單純地由上而下依序執行程式碼,且一次只執行一件事,需要等待事情執行完畢,才能繼續往下,如同上述提到麵店老闆的例子。

而「非同步」則盡可能讓主要執行程序不需停下來等待,當程式遇到非同步行為如AJAX或setTimeout,會先發起一個「非同步處理」把事件移到事件佇列(Event Queue)後繼續執行程式碼,等所有程式執行完成後,最後再回頭處理非同步行為。

source from AlphaCamp

Callback則是最初被提出用來處理非同步行為的一種方式,如果要使非同步「依序」完成,就得在callback中加入另外一個callback,形成巢狀結構而導致callback hell的狀況發生。從 ES2015 (ES6) 之後Promise 成為 JavaScript 的標準,改善Callback的缺點,提升可讀性,因此現今在開發時多以Promise取代callback的用法。

何謂Promise?

試著將每段非同步程式碼看作是一項任務,無論任務是否成功與否,都會承諾你給出一個結果,而Pomise就是由非同步function處理完任務後所return的結果(為一個物件)。

Promise 是一個物件建構子 (constructor),使用時需要先從 Promise 物件產生物件實例 (instance),再使用繼承特性的 instance 去包裝程式碼的 callback 流程。*建構子的介紹可參考此篇文章

//自定義一個promise物件
const promise = new Promise((resolve, reject) => {
if(value){
resolve('成功')
}else {
reject('失敗')
}
});

Promise的狀態

source from MDN

Promise在執行過程中會有三種狀態:
1. pending : 事件進行中,尚未有結果
2. resolved : 事件已完成,執行成功並回傳成功的結果
3. rejected:事件已完成,執行失敗並回傳失敗的結果

一旦進入了resolved或rejected,此promise就不會再改變,可以用.then或.catch來獲得回傳值。我們可以透過 console.dir(promise())來查看物件狀態及回傳值。

//測試不設定回傳結果時的狀態
function promise(value) {
return new Promise((resolve, reject) => {
// if(value){
// resolve('成功')
// }else {
// reject('失敗')
// }
});
}
console.dir(promise());
回傳的狀態為pending

Chained Promises

.then()、.catch()、.finally()皆為promise物件的方法,可以透過鏈接的方式來取得上一段程式碼的回傳值進行下個任務,也增加了易讀性,規則為:

  1. 方法不限於promise結構,任何表達式皆可放置於return後方
  2. 若是promise則繼續遵循.then、.catch的方式
  3. 非promise物件則可在下個.then中取得return的值
  4. .then(onFulfilled, onRejected) 最多接兩個參數,分別為成功或失敗的callback function
  5. 失敗的回傳資訊也可以改用.catch(onRejected)分開來鏈接
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 300);
});

myPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB)

//or
myPromise
.then(handleFulfilledA()=>{
// Promise回傳正確, 此處return的值可由下一個then承接
return handleFulfilledB
})
.then(handleFulfilledB()=>{
// 接收前面的回傳值
console.log(`接收handleFulfilledA的回傳值:${handleFulfilledB}`)
})
.catch(handleRejectedA) // Promise回傳失敗
.finally(()=>console.log('promise執行完畢'))//無論成功失敗,非同步執行完畢即執行

在多層.then()的串接下,只要有錯誤發生就會值接跳到.catch()的部分,中間就不會再執行了(因為也沒有正確的回傳值傳給下一個return進行處理)

要注意的是,並非所有.then()都是有關聯的,.then()的主要目的在於流程控制,某一件事情做完之後,透過.then()來去執行下一件事。

  Category.find()
.lean()
.then((categories) => { //此處的.then()和前方promise物件是有關聯的
res.render("new", { categories });
})
.catch((e) => console.log(e));

Promise的方法

  1. Promise.all(Array): 傳入陣列當作參數,同時執行所有promises,當全部執行完成後回傳包含所有promises回傳值的Array順序和傳入時一致
  2. Promise.race(Array): 傳入陣列當作參數,同時執行所有promises,只回傳第一個完成的。

當有多個promises要處理,我們希望這些promises全部處理完成後,才進行下一步時,可以使用Promise提供的方法並把promises放入陣列中當作參數,來控制流程。

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});

Promise.all([p1, p2, p3])
.then(values => {
console.log(values); // [3, 1337, "foo"]
})
.catch(e=>console.log(e));

而當陣列中其中一個promise的結果為reject時,兩種方法都會直接進入到onRejected的狀態,也就是直接進到.catch回傳錯誤訊息,而不會有.then的結果。

繼Promise之後,javascript又推出了下一代非同步處理的解決方案-async / await,使非同步語言看起來像同步語言,可以更簡單、更直覺地呈現出程式碼的流程,待下篇再來介紹。

參考:
Promise MDN
看完這本就會懂!帶你無痛提升JavaScript面試力:精選55道前端工程師的核心問題 × 求職加分模擬試題解析

非同步處理三部曲:

  1. 何謂Callback function?
  2. 避免Callback Hell的Promise
  3. 使閱讀更直覺的Async / await

--

--

Kim.H

現任菜鳥後端工程師 / 2022.12 正式踏入轉職之旅 - 2023.09轉職成功