ES6: 用 Promise 處理非同步問題的解題技巧

透過 Promise 處理非同步操作,可確保事件完成後再執行。我們會在 Promise 建構函式中使用 resolve 或 reject 來控制執行流程。當執行非同步事件時,Promise 會維持 pending 狀態,直到事件完成並呼叫 resolve 成為 fulfilled 或因錯誤而呼叫 reject 成為 rejected。使用 then、catch、finally 來處理 Promise 的結果,以確保非同步操作的正確性和時序。

ES6: 用 Promise 處理非同步問題的解題技巧
Photo by Gilles Lambert / Unsplash

ES6 新增了一個新語法:Promise。當我們在 JavaScript 使用 fetch() 函式傳入一段 url,它會 return 一個 Promise,這個 Promise 是一個獨特的物件。

當非同步發生時,如果我們想要確保執行的順序是在完成非同步事件後,才往下執行的話就可以用 Promise 處理。(過去會用 Callback, ES7 則可以用語法糖 Async Await)


一、Promise 建構函式

Promise 這個函式必須傳入一個參數 (該參數為函式),這參數函式又包含兩個參數(都為函式),分別為 resolve 和 reject

resolvereject,這兩個函式分別代表成功與失敗的回傳結果,且這兩個方法是 JaveScript 引擎幫我們準備好了,無需再額外定義,但我們要注意的地方為兩個函式只能回傳一個,當回傳結果後,一個 Promise 就代表結束了。

function Promise((resolve, reject) => { 
	resolve() // 正確完成的回傳方法
	reject()  // 失敗的回傳方法
})

二、Promise 物件: 以 Promise 建構函式為原型

Promise 物件以 Promise 建構函式為原型,並使用 new Promise() 建立的物件
const p = new Promise()

三、Promise 物件的狀態

Promise 本身有三種狀態: pending、fulfilled、rejected

Promise 的狀態一開始會是 pending , 一旦 resovle() 被使用,狀態就會轉變為 fulfilled,而 reject() 被使用,狀態就會被轉變為 rejected。

  1. pending (擱置)
    • 事件已經運行中,尚未取得結果
  2. fulfilled (完成、實現)
    • 事件已經執行完畢且成功操作,回傳 resolve 的結果
    • 意思是該承諾 Promise 已經被實現 fulfilled
  3. rejected (拒絕)
    • 事件已經執行完畢但操作失敗,回傳 rejected 的結果
let cookFoodPromise = (foodName, time) => {
  return new Promise((resolve, reject) => {
    if (time > 3 && 5 > time) {
      resolve(`${foodName}完美`)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}

四、Promise 物件的方法

Promise 物件一旦建立起來就有 then、catch、finally 等方法可以呼叫
let cookFoodPromise = (foodName, time) => {
  return new Promise((resolve, reject) => {
    if (time > 3 && 5 > time) {
      resolve(`${foodName}完美`)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}

const cookTime = parseInt(Math.random() * 10)  // 隨機帶入分鐘
cookFoodPromise('泡麵', cookTime)
.then((res)  => { console.log(res) })          // "泡麵完美"
.catch((err) => { console.log(err) })          // "泡麵失敗"
.finally(()  => { console.log('done')})        // "done"

// 若是沒執行 resolve() 也沒執行 reject()
console.log(cookFoodPromise('泡麵', cookTime)) // Promise {<pending>}
使用 then 來串接上一個的結果繼續做事情
let cookFoodPromise = (foodName, time) => {
  return new Promise((resolve, reject) => {
    if (time > 3 && 5 > time) {
      resolve(`${foodName}完美`)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}

const cookTime = parseInt(Math.random() * 10) // 隨機帶入分鐘
cookFoodPromise('泡麵', cookTime)
.then((res)  => { return res + '好吃'} )
.then((res)  => { console.log(res)})          // "泡麵完美好吃"
.catch((err) => { console.log(err) })         // "泡麵失敗"
.finally(()  => { console.log('done')})       // "done"

五、Promise 物件進階方法: Promise.all()

Promise.all() 可以同時執行大量 Promise 物件,並且在全部完成後回傳陣列

一旦有 Promise 物件失敗,將回傳失敗那個物件回傳的結果,如果是全部失敗,則回傳第一個 Promise 物件的失敗結果,來當成整個最後的錯誤訊息。

這個方法很適合用在多支 API 要一起執行,並確保全部完成後才進行其他工作時。

1. 成功

let cookFoodPromise = (foodName, cookTime, timer) => {
  return new Promise((resolve, reject) => {
    if (cookTime > 3 && 6 > cookTime) {
      setTimeout(() => {
        resolve(`${foodName}完美`)
      }, timer)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}

let arr = [cookFoodPromise('花雕雞泡麵', 5, 1500), cookFoodPromise('麻油雞泡麵', 5, 3500)]
Promise.all(arrCookFoodPromise)
.then(res => console.log(res)) // ["花雕雞泡麵完美", "麻油雞泡麵完美"]

2. 一次失敗

let cookFoodPromise = (foodName, cookTime, timer) => {
  return new Promise((resolve, reject) => {
    if (cookTime > 3 && 6 > cookTime) {
      setTimeout(() => {
        resolve(`${foodName}完美`)
      }, timer)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}
let arr = [cookFoodPromise('花雕雞泡麵', 5, 2500), cookFoodPromise('麻油雞泡麵', 2, 1500)]
Promise.all(arr)
.then(res => console.log(res))
.catch(err => console.log(err)) // "麻油雞泡麵失敗"

3. 全部失敗

let cookFoodPromise = (foodName, cookTime, timer) => {
  return new Promise((resolve, reject) => {
    if (cookTime > 3 && 6 > cookTime) {
      setTimeout(() => {
        resolve(`${foodName}完美`)
      }, timer)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}
let arr = [cookFoodPromise('花雕雞泡麵', 2, 2500), cookFoodPromise('麻油雞泡麵', 2, 1500)]
Promise.all(arr)
.catch(err => console.log(err)) // "花雕雞泡麵失敗"

六、Promise 物件進階方法: Promise.race()

Promise.race() 只要有一個 Promise 物件回傳結果,不論成功或失敗都會結束該次 呼叫

透過陣列的形式傳入多個 promise 函式,在全部執行完成後回傳單一結果,結果為第一個運行完成的。(回傳最快回應的結果)

1. 成功

let cookFoodPromise = (foodName, cookTime, timer) => {
  return new Promise((resolve, reject) => {
    if (cookTime > 3 && 6 > cookTime) {
      setTimeout(() => {
        resolve(`${foodName}完美`)
      }, timer)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}

let arr = [cookFoodPromise('花雕雞泡麵', 5, 4500), cookFoodPromise('麻油雞泡麵', 5, 3500)]
Promise.race(arr)
.then(res => console.log(res))    // "麻油雞泡麵完美"
.catch(err => console.log(err))

2. 失敗

let cookFoodPromise = (foodName, cookTime, timer) => {
  return new Promise((resolve, reject) => {
    if (cookTime > 3 && 6 > cookTime) {
      setTimeout(() => {
        resolve(`${foodName}完美`)
      }, timer)
    } else if (time <= 3) {
      reject((`${foodName}失敗`))
    }
  })
}

let arr = [cookFoodPromise('花雕雞泡麵', 2, 1500), cookFoodPromise('麻油雞泡麵', 5, 3500)]
Promise.race(arr)
.then(res => console.log(res))
.catch(err => console.log(err))   // "花雕雞泡麵失敗"

參考資料
1. 你今天 Promise 了嗎?
2. JavaScript Promise 全介紹
3. Promise 的使用
4. JavaScript - Promise (2)