五倍紅寶石・專業程式教育

五倍紅寶石 ・專業程式教育機構

你今天 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 老師

訂閱文章

  • 五倍的五倍券優惠活動