iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Modern Web

入門前端技能之旅系列 第 28

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

「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", function(){
  // 設定按按鈕後一秒後抽獎
  setTimeout( function () {
    let n = randomNum(1, 100);  // 1~100 隨機數
    if ( n <= 30 ) {
      console.log("恭喜你中獎了!你的中獎數字是" + n);  // 30% 中獎率
    } else {
      console.error("銘謝惠顧~你的數字是" + n)
    }
  }, 1000)
})

Promise

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

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

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

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

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

/* 修改事件監聽,進行 Promise 包裝 */

btn.addEventListener("click", function(){

  const p = new Promise((resolve, reject) => {  // Promise 包裝
    
    setTimeout(() => {
      let 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) => {
                let 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 還有很多方法還沒講到,目前只是初體驗!

參考資料


上一篇
Day.27 「var 成為時代的眼淚!」 —— ES6 區塊作用域 與 提升
下一篇
Day.29 「Class 語法糖~」 —— ES6 Class 構造函式
系列文
入門前端技能之旅30

尚未有邦友留言

立即登入留言