你今天 Promise 了嗎?

By 哲昱・5月 5 2021・技術文章
你今天 Promise 了嗎?

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,例如: requestresponse。就如上述 Fetch 就是基於 ECMAScript6 Promise 語法結構開發出來了的技術,因為如此,我們可以無痛使用,且彈性較高。

何謂 Promise ?

Promise 為建構函式,此函式必須傳入一個參數(該參數為函式),這參數函式又包含兩個參數(都為函式),分別為 resolvereject 。 而 resolvereject,這兩個函式分別代表成功與失敗的回傳結果,且這兩個方法是 JaveScript 引擎幫我們準備好了,無需再額外定義,但我們要注意的地方為兩個函式只能回傳一個,當回傳結果後,一個 Promise 就代表結束了。

而 Promise 大多使用在非同步處理,至於何謂同步和非同步,大家可以參閱另外一篇文章 無痛理解 JS | 非同步怎麼運作?,相信大家就可以有基本了解。

何謂 Promise 物件 ?

Promise 物件是以 Promise 建構函式為原型,並使用 new Promise() 建立起來的物件。

Promise 物件一旦建立起來就有 thencatchfinaly 等方法可以呼叫。

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 吧!

舉個例子:

Photo by 甲上,2017.04.18

泡麵,有專屬於它的烹調時間,這就來用程式來判別一個泡麵是否有完美成功, 首先我們準備了 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()

raceall 不同的是只要有一個 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 老師