
Photo by Kun Fotografi from Pexels
前言
近期跟學員們介紹如何使用 Fetch API 串接資料時,發現同學對於回傳的 Promise 物件似乎有很大的疑惑。藉此來介紹何謂 Promise,也收集了一些同學們的常見問題來展開初探 Promise 之旅。
何謂 Fetch API
為何 Fetch API 回傳會是個 Promise 物件 ?
比起過去常使用的 XMLHttpRequest,Fetch 的設計角度和使用方法都很不同,而 Fetch 實際在取得遠端資訊時,語法相較 XMLHttpRequest 簡潔且過程容易許多,卻可以達到相同的效果。
至於 Fetch API 為何是 Promise 物件 ?
首先科普一下 Fetch API 是提供 JavaScript Interface 來處理 HTTP pipeline,例如: request
和 response
。就如上述 Fetch 就是基於 ECMAScript6 Promise 語法結構開發出來了的技術,因為如此,我們可以無痛使用,且彈性較高。
何謂 Promise ?
Promise 為建構函式,此函式必須傳入一個參數(該參數為函式),這參數函式又包含兩個參數(都為函式),分別為 resolve
和 reject
。 而 resolve
和 reject
,這兩個函式分別代表成功與失敗的回傳結果,且這兩個方法是 JaveScript 引擎幫我們準備好了,無需再額外定義,但我們要注意的地方為兩個函式只能回傳一個,當回傳結果後,一個 Promise 就代表結束了。
而 Promise 大多使用在非同步處理,至於何謂同步和非同步,大家可以參閱另外一篇文章 無痛理解 JS | 非同步怎麼運作?,相信大家就可以有基本了解。
何謂 Promise 物件 ?
Promise 物件是以 Promise 建構函式為原型,並使用 new Promise()
建立起來的物件。
Promise 物件一旦建立起來就有 then
、catch
、finaly
等方法可以呼叫。
const promiseObject = new Promise((ressolve, reject)=>{})
promiseObject.then() : 接收 Promise 回傳正確的結果。
promiseObject.catch() : 接收 Promise 回傳失敗的結果。
promiseObject.finaly() : 無論正確或失敗都會回傳。
Promise 狀態
Promise 的狀態有三種:
1. pending
2. fulfilled
3. rejected
Promise 的狀態一開始會是 pending
, 一旦 resovle()
被使用,狀態就會轉變為 fulfilled
,而 reject()
被使用,狀態就會被轉變為 rejected
。
這裡需注意的地方為 Promise 只有結果會影響狀態,無法透過一般外力操作使其更改狀態,而狀態一旦從 pending
改變後,就無法改變了。
Promise 用法
試著建立一個 Promise 吧!
舉個例子:
泡麵,有專屬於它的烹調時間,這就來用程式來判別一個泡麵是否有完美成功,
首先我們準備了 cookFoodPromise function
來執行我們的完美料理,而這個 function
裡面也有一個承諾 (Promise) 會告知我們每煮出來的一碗泡麵是否是完美的,絕對不會破壞我們的用餐體驗,這時我們只要帶入兩個參數,菜名和烹調時間(分鐘計),時間會隨機產生,如果在 3 和 5 分鐘之間,就會完美成功,其他則會失敗。
let cookFoodPromise = (foodName, time) => {
return new Promise((resolve, reject) => {
if (time > 3 && 5 > time) {
resolve(`${foodName} 完美`)
} else {
reject((`${foodName} 失敗`))
}
})
}
const cookTime = parseInt(Math.random() * 10) // 隨機帶入分鐘
cookFoodPromise('泡麵', cookTime)
.then((res) => { console.log(res) }) // 泡麵 完美
.catch((err) => { console.log(err) }) // 泡麵 失敗
當執行完 cookFoodPromise function
後,如果完美泡麵被煮出時,立刻上桌享用,此時可以使用 then()
這個托盤(方法),來接這碗泡麵 (Promise 回傳 resolve
的結果)。
不完美的話,則使用 catch()
這個廚餘桶(方法)來處理這碗泡麵 (Promise 回傳 rejected
的結果)。
Promise 鏈接
不僅僅是上述例子如此,我們還可以使用 then
來串接上一個的結果繼續做事情,如下:
let cookFoodPromise = (foodName, time) => {
return new Promise((resolve, reject) => {
if (time > 3 && 5 > time) {
resolve(`${foodName} 完美`)
} else {
reject((`${foodName} 失敗`))
}
})
}
const cookTime = parseInt(Math.random() * 10) // 隨機帶入分鐘
cookFoodPromise('泡麵', cookTime)
.then((res) => { return res + '好吃'} )
.then((res) => {console.log(res)}) // "泡麵 完美好吃"
.catch((err) => { console.log(err) }) // "泡麵 失敗"
但是我們在某個階段發生錯誤時,該階段下一個是 then
的話就不會執行,會直接跳到 catch
。
其實 catch
執行完畢後,還是可以繼續使用 then
串接,但在實務上我們很少會這麼做,如下:
let cookFoodPromise = (foodName, time) => {
return new Promise((resolve, reject) => {
if (time > 3 && 5 > time) {
resolve(`${foodName} 完美`)
} else {
reject((`${foodName} 失敗`))
}
})
}
const cookTime = parseInt(Math.random() * 10) // 隨機帶入分鐘
cookFoodPromise('麻油雞泡麵', cookTime)
.then((res) => { return cookFoodPromise('花雕雞泡麵', 6)} ) //煮第一碗成功,接著煮第二碗固定失敗
.then((res) => {console.log(res)}) // 不執行
.catch((err) => {return err+ '準備'})
.then((failure) => {console.log(failure + '倒進廚餘桶')}) //第一碗就失敗:"麻油雞泡麵 失敗準備倒進廚餘桶" 第二碗才失敗:"花雕雞泡麵 失敗準備倒進廚餘桶"
Promise.all()
有時候我們可能會想同時享用多碗泡麵,這時候就需要使用複數個爐台,這個複數爐台就是 Promise.all()
,其背後操作則是使用陣列將多個 promise
函式打包,當全部執行完成後回傳陣列結果,而陣列的結果順序與一開始傳入的一樣。 但是一旦有 Promise 物件失敗,將回傳失敗那個物件回傳的結果,如果是全部失敗,則回傳第一個 Promise 物件的失敗結果,來當成整個最後的錯誤訊息。
成功:
let cookFoodPromise = (foodName, cookTime, timer) => {
return new Promise((resolve, reject) => {
if (cookTime > 3 && 6 > cookTime) {
setTimeout(() => {
resolve(`${foodName} 完美`)
}, timer)
} else {
reject((`${foodName} 失敗`))
}
})
}
Promise.all([cookFoodPromise('花雕雞泡麵', 5, 1500), cookFoodPromise('麻油雞泡麵', 5, 3500)])
.then(res => console.log(res)) // ["花雕雞泡麵 完美", "麻油雞泡麵 完美"]
失敗:
一個失敗的例子:
let cookFoodPromise = (foodName, cookTime, timer) => {
return new Promise((resolve, reject) => {
if (cookTime > 3 && 6 > cookTime) {
setTimeout(() => {
resolve(`${foodName} 完美`)
}, timer)
} else {
reject((`${foodName} 失敗`))
}
})
}
Promise.all([cookFoodPromise('花雕雞泡麵', 5, 2500), cookFoodPromise('麻油雞泡麵', 2, 1500)])
.then(res => console.log(res))
.catch(err => console.log(err)) // "麻油雞泡麵 失敗"
全部失敗的例子:
let cookFoodPromise = (foodName, cookTime, timer) => {
return new Promise((resolve, reject) => {
if (cookTime > 3 && 6 > cookTime) {
setTimeout(() => {
resolve(`${foodName} 完美`)
}, timer)
} else {
reject((`${foodName} 失敗`))
}
})
}
Promise.all([cookFoodPromise('花雕雞泡麵', 2, 2500), cookFoodPromise('麻油雞泡麵', 2, 1500)])
.catch(err => console.log(err)) // "花雕雞泡麵 失敗"
Promise.race()
race
和 all
不同的是只要有一個 Promise 物件回傳結果,不論成功或失敗,都會結束該次 Promise.race() 呼叫。
成功:
let cookFoodPromise = (foodName, cookTime, timer) => {
return new Promise((resolve, reject) => {
if (cookTime > 3 && 6 > cookTime) {
setTimeout(() => {
resolve(`${foodName} 完美`)
}, timer)
} else {
reject((`${foodName} 失敗`))
}
})
}
Promise.race([cookFoodPromise('花雕雞泡麵', 5, 4500), cookFoodPromise('麻油雞泡麵', 5, 3500)])
.then(res => console.log(res)) // "麻油雞泡麵 完美"
.catch(err => console.log(err))
失敗:
let cookFoodPromise = (foodName, cookTime, timer) => {
return new Promise((resolve, reject) => {
if (cookTime > 3 && 6 > cookTime) {
setTimeout(() => {
resolve(`${foodName} 完美`)
}, timer)
} else {
reject((`${foodName} 失敗`))
}
})
}
Promise.race([cookFoodPromise('花雕雞泡麵', 2, 1500), cookFoodPromise('麻油雞泡麵', 5, 3500)])
.then(res => console.log(res))
.catch(err => console.log(err)) // "花雕雞泡麵 失敗"
結語
今天跟大家簡單介紹 Promise 以及其用法,相信學習 Promise 後,對於非同步處理就再也不那麼陌生,整個流程控制也會掌握得比較好,後續有機會的話,可以和大家來介紹專屬於它的語法糖衣 - Async
& Await
,希望透過本篇大家對 Promise 可以有初步的了解 😀
👩🏫 課務小幫手:
✨ 想掌握 JavaScript 觀念和原理嗎?
我們近期也有開設 JavaScript 課程喔! 👉 https://reurl.cc/KxGnVq
講者是『 0 陷阱!0 誤解!8 天重新認識 JavaScript!』作者 - 🧑💻 Kuro 老師