跳至主要內容

Day.28 「Promise 初體驗~」 —— ES6 Promise

約 5 分鐘

「Promise 初體驗~」 —— ES6 Promise

我們前面已經學習了回調函式(Callback Function)與構造函式(Constrcutor),而 Promise 是 ES6 新增用來解決非同步回調地域的新語法,同時也是一個構造函式!

非同步

在這裡我們要先了解到什麼是非同步!相信大家應該都聽過最好理解的範例,那就是用餐廳來做範例!

同步的概念就像是:服務生接收點餐 → 通知廚房有餐 → 廚房完成餐點 → 結帳 → 接下一位客人 一步一步做下去,優點是不易出錯,但缺點也非常明顯,效率非常差。

而非同步的概念:服務生接收點餐 → 通知廚房有餐 → 結帳 → 接下一位客人 → … → 廚房完成餐點一起給客人 能夠把需要先執行的優先執行,優點就是效率好,但缺點就是跟同步比起來,維護比較麻煩。

而在我們介紹定時器時,就有體現出非同步的狀態。

function order() {
  console.log('點餐');
 
  (function making() {
    console.log('開始製作');
 
    (function checkout() {
      console.log('結帳')
    })()
    setTimeout(() => {
      console.log('餐點完成')
    }, 1000)
  })()
}
 
order()
order()
 
/*
    "點餐"
    "開始製作"
    "結帳"
    "點餐"
    "開始製作"
    "結帳"
    "餐點完成"
    "餐點完成"
*/

這時就有點看到回調地獄(Callback Hell)的影子了!這就是它不容易維護的部分

  • 不易閱讀
  • 處理異常處理不方便

而 Promise 改善了回調地獄的問題。

ES6 以前

在還沒有 ES6 前,處理 AJAX 與 計時器的時候,都是直接使用回調函式來處理非同步事件 這裡用抽獎為例

<!-- HTML -->
<button id="btn">點我抽獎</button>
const btn = document.getElementById('btn') // 獲取按鈕 DOM
 
/*  隨機數函式  */
function randomNum(m, n) {
  return Math.ceil(Math.random() * (n - m + 1)) + m - 1
}
 
btn.addEventListener('click', () => {
  // 設定按按鈕後一秒後抽獎
  setTimeout(() => {
    const n = randomNum(1, 100) // 1~100 隨機數
    if (n <= 30) {
      console.log(`恭喜你中獎了!你的中獎數字是${n}`) // 30% 中獎率
    }
    else {
      console.error(`銘謝惠顧~你的數字是${n}`)
    }
  }, 1000)
})

Promise

在 ES6 之後,可以透過 Promise 來包裝程式碼, 而使用 Promise 的方式,與構造函式的使用方式類同,而參數帶入的是函式,帶入的函式內會有兩個參數 resolve 、 reject。

const p = new Promise((resolve, reject) => {
  // ...
})

這兩個參數本身也是函式,一個代表解決,一個代表拒絕,函式的參數可以進行傳遞。

const p = new Promise((resolve, reject) => {
  if ('成功') {
    resolve('成功') // 成功使用 resolve 函式,代表這個 Promise 物件的狀態是成功的
  }
  else {
    reject('失敗') // 失敗使用 reject 函式,代表這個 Promise 物件的狀態是失敗的
  }
})

以上面的抽獎例子做修改。

/* 修改事件監聽,進行 Promise 包裝 */
 
btn.addEventListener('click', () => {
  const p = new Promise((resolve, reject) => { // Promise 包裝
    setTimeout(() => {
      const n = randomNum(1, 100) // 1~100 隨機數
      if (n <= 30) {
        resolve(n) // 將 Promise 物件設定為"成功" n 作為資料參數傳遞出去
      }
      else {
        reject(n) // 將 Promise 物件設定為"失敗" n 作為資料參數傳遞出去
      }
    }, 1000)
  })
})

這樣就包裝好了,但你會發現,奇怪怎麼沒有效果了? 那是因為還要調用 then 方法,來接收成功或失敗的資料,一樣可以接收兩個參數,兩個參數分別代表成功與失敗的函式,而成功與失敗的函式可以靠參數傳遞資料。

then

p.then((data) => {
  console.log(`恭喜你中獎了!你的中獎數字是${data}`)
}, (err) => {
  console.error(`銘謝惠顧~你的數字是${err}`)
})

你可能覺得,好像沒有方便到哪裡呀~還要另外用 then 來調用! 那是因為我們這個範例還很簡單,沒有到 Callback Hell 的程度,當資料越來越複雜,就會形成 Callback Hell。

// node.js 資料串接
data.readFile('./data/a.text', (err, data1) => {
  data.readFile('./data/b.text', (err, data2) => {
    data.readFile('./data/c.text', (err, data3) => {
      data.readFile('./data/d.text', (err, data4) => {
        const result = data1 + data2 + data3 + data4
        console.log(result)
      })
    })
  })
})

catch

而 Promise 只要包裝好了,接下來只要使用 then 來進行連續調用串接,不會讓程式碼越來越往右推移。 此外大多數情況,也不會刻意接失敗的資料,可以依靠 catch 來進行最後失敗時的處理

// 先在最外層進行 Promise 包裝
const p = new Promise((res, rej) => {
  data.readFile('./data/a.text', (err, data) => {
    res(data)
  })
})
// 使用 then 串接
p.then((val) => {
  return new Promise((res, rej) => {
    data.readFile('./data/b.text', (err, data) => {
      res([val, data])
    })
  })
}).then((val) => {
  return new Promise((res, rej) => {
    data.readFile('./data/c.text', (err, data) => {
      val.push(data)
      res(val)
    })
  })
}).then((val) => {
  console.log(val) // 成功使用 then
}).catch((err) => {
  console.error('串接失敗!') // 失敗使用 catch
})

總結

雖然短期這樣看,Promise 寫起來好像沒有 Callback 快,但它解決了長期的資料變龐大的時候,所產生的回調地獄,Promise 只是向下添加程式碼,而 Callback Hell 則是一直往右推移程式碼,Promise 還有很多方法還沒講到,目前只是初體驗!

參考資料

相關文章

分享此文章
Johnson Mao

Johnson Mao

熱衷於分享技術、AI 與開發心得的軟體工程師。

留言