前端基礎

-

前端重要觀念 Promise - 為甚麼要有 Promise?如何使用?

this.web

到底甚麼是 Promise?

用一句話說,Promise 是一個對 還未有結果的事物 的一個替身,且他不是前端才有的東西。

替身

那為甚麼要對未有結果的事物做一個替身呢?

這篇文有提到,我們不知道非同步的程式什麼時候會回傳東西過來,這種不確定性讓我們沒辦法對他有很好的操作,例如回傳來失敗我們也無法知道,但 Promise 就可以很好的解決這個問題。

同步 vs 非同步

簡單複習一下,程式碼有分同步和非同步:

  • 同步:一件事情做完接著做另一件事情,比如先刷牙在洗澡。
  • 非同步:一次做多件事情,比如同時刷牙和洗澡。
sync vs async

我們無法確定非同步什麼時候會結束,就好比我們不確定會先刷完牙還是洗完澡,只有真的結束的時候,才會知道。

且回調函數會有回調地獄的問題,並且回調函數也沒有一個規範,每個人的寫法都不一樣,造成使用和閱讀上的困難。 為了解決這些問題,有了 Promise 的誕生。

大陸翻譯叫做期約,台灣大多數都直接說 Promise

如何創建 Promise

要使用 Promise 的方法很簡單 👇

let p = new Promise(() => {})
console.log(p) // Promise {<pending>}

因為 Promise 的特性,我們必須要傳一個函數當作參數給他,不然會報錯,這個等等會提到。

💡 複習:根據我們對 class、建構式的理解,可以知道這個 p 是一個 Promise 的實例,而且應該會有自己的屬性和函式

Promise 狀態

眼尖的你有發現前面 console.log(p) 後有一個 {<pending>} 嗎?這是 Promise 的狀態,那為甚麼要有這個狀態呢?

還記得 Promise 是對不存在結果的一個替身嗎,這個狀態就是為了形容這個結果的!

Promise 有三種狀態:

  1. pending (待定):還未回傳結果
  2. fulfilled (兌現):回傳成功,又被稱為 resolved (解決)
  3. rejected (拒絕):回傳失敗

這樣我們就可以根據這三種狀態來去處理資料。

Promise 處理狀態

那我們要如何根據這三種狀態來去處理 Promise 呢?其實 promise 的狀態是私有的,我們沒辦法透過以下這種方式來去修改:

p.PromiseState = 'pending'

這也很好理解,因為如果可以這樣亂改那整個程式都要亂掉了,所以前面才說要傳入一個函數當作參數,因為我們要像下面這樣,在 Promise 的內部來去操作 Promise 的狀態:

function createPromise(success) {
  return new Promise((resolve, reject) => {

    // 處理資料...

    if (success) {
      resolve('資料回傳成功!')
    }
    else {
      reject('資料回傳失敗!')
    }

  })
}

接著我們先來自己操作 success 來試試看

let p1 = createPromise(true);
console.log(p1); // Promise {<fulfilled>: '資料回傳成功!'}

let p2 = createPromise(false);
console.log(p2) // Promise {<rejected>: '資料回傳失敗!'}

上面的 resolve()reject() 就是告訴 promise 現在是成功還是失敗,如果  success 則使用 resolve() 代表成功,反之則使用 reject() 代表失敗。有發現我們 console 出來後有顯示 fulfilledrejected 嗎? 這就是成功和失敗的狀態,而 Promise 的狀態只要一改變就沒辦法再變回去 pending 了

Promise 如何處理回傳的資料

那我們要怎麼使用取得非同步傳回來的資料呢?只要使用 .then() 就可以了

p1 = createPromise(true)
  .then(data => console.log(data));
  // '資料回傳成功!'

.then( ) 裡面要放一個函數 而這個函數的參數就是傳給 resolve() 裡的參數。

那這個 .then 到底是甚麼意思? 還記得上次回調函數的方式嗎,取得第一筆資料,然後再用第一筆資料的地址抓第二筆資料。

這個然後就是 .then()

就像是"我先吃早餐,然後上班的"這個然後,這也是 promise 厲害的地方,他可以等 resolve() 結束,然後接著做其他事情。

如果 Promise 回傳資料失敗怎麼辦? .catch()

前面的 .then() 是 promise 成功 (fullfilled) 才會調用,這也很好理解,都失敗了還有甚麼然後勒 ,所以我們要用 .catch() 來處理失敗的狀況,catch 就是抓住錯誤的意思,比如以下程式碼:

p2 = createPromise(false)
  .then(data => console.log(data))
  .catch(err => console.log(err)); 
  // '資料回傳失敗'
❗注意,只要發生錯誤,就會直接執行 .catch() 而跳過所有的 .then()

連鎖 promise

有時候我們 promise 成功後想要執行東西,執行成功後又想要執行其他東西,就可以不斷的 .then() 來做,例如:

let p = new Promise((resolve, reject) => { 
 console.log('first'); 
 resolve(); 
}); 

p.then(() => console.log('second'))
  .then(() => console.log('third'))
  .then(() => console.log('fourth'));
// first 
// second 
// third 
// fourth

不過這沒什麼太大的意義,因為直接寫同步的函數也可以做到:

(() => console.log('first'))(); 
(() => console.log('second'))(); 
(() => console.log('third'))(); 
(() => console.log('fourth'))(); 

所以連鎖 promise 厲害的地方是用在多個非同步程式碼,在 .then() 的地方回傳一個新的 promise 這樣就可以確保每個函數都會等待之前promise 完成。

我用以下程式碼做示範,為了方便,我們先寫一個製作 promise 的函數,並利用 setTimeout 模擬非同步。

💡setTimeout 會讓瀏覽器去計時,這個時候 JS 先執行其它內容,等瀏覽器計時完後告訴 JS,讓 JS 執行傳入的函數,所以 setTimeout 就是一個非同步的例子。
function createPromise(seconds, data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(data);
      resolve();
    }, seconds * 1000);
  });
}

接著來用 .then 依序執行

let p1 = createPromise(1, 'p1');
p1.then(() => createPromise(1, 'p2'))
  .then(() => createPromise(1, 'p3'))
  .then(() => createPromise(1, 'p4'));
// p1 (1 秒後)
// p2 (2 秒後)
// p3 (3 秒後)
// p4 (4 秒後)

利用 Promise 改寫回調地獄

在這 你知道非同步嗎 貼文中,我們用回調函數示範了回調地獄,也就是

function getSomeData(url, callback) {
  // 獲取 data

  callback(yourData);
}

getSomeData(url1, (data1) => { 
  getSomeData(data1.url, (data2) => {
    getSomeData(data2.url, (data3) => {
      getSomeData(data3.url, (data4) => {
        ...
      })
    });
  })
})

// other operation

我們用 Promise 來改寫:

function getSomeDataPromise(url) {
  return new Promise((resolve, reject) => {
    // 利用 url 獲取 yourData
    if (success) {
      resolve(yourData);
    }
    else {
      reject('獲取錯誤')
    }
  })
}
getSomeDataPromise(url)
  .then(data1 => {
    console.log(data1);
    return getSomeDataPromise(data1.url)
  })
  .then(data2 => {
    console.log(data2);
    return getSomeDataPromise(data2.url)
  })
  .then(data3 => {
    console.log(data3);
    return getSomeDataPromise(data3.url)
  })
  .catch(err => {
    console.log(err)
  })

可以發現利用 Promise.then() 比直接使用 callback 簡潔的多。

Promise 的其它方法

除了 resolve()reject() 以外,promise 還有其他好用的函數像是

  1. all()
  2. allSettled()
  3. race()
  4. finally()

和上次提到的 promise 連鎖不同,all()race() 是利用合成的方式來處理 promise,我們直接看例子會比較清楚。

Promise.all()

all() 會將多個 promise 組合起來,等到全部的 promise 都完成執行後才會再執行all(),用上次連鎖 promise 的例子 👇

function createPromise(seconds, data, success = true) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      
      if (success) {
        resolve(data);
      }
      else {
        reject('err');
      }
      
    }, seconds * 1000);
  });
}

let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2');
let p3 = createPromise(3, 'p3');

Promise.all([p1, p2, p3]).then(res => console.log(res))
// 3 秒後回傳 [p1, p2 ,p3]

因為用 Promise.all 回傳的資料順序和你傳入的順序相同的
所以這個方法適合用在同時執行多個 API ,且希望回傳的順序照是你預期的情況。

但若中間有失敗的 promise,就只會回傳失敗的訊息。

function createPromise(seconds, data, success = true) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      
      if (success) {
        resolve(data);
      }
      else {
        reject('err');
      }
      
    }, seconds * 1000);
  });
}

let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2', false);
let p3 = createPromise(3, 'p3');

Promise.all([p1, p2, p3])
  .then((res) => console.log(res))
  .catch((err) => console.log(err));
// 'err'

Promise.allSettled()

這是 ES11 新增的語法,是為了解決 Promise.all() 的缺點。

前面說 Promise.all() 失敗的時候只會返回失敗的訊息,但有時候我們可能成功失敗的資料都需要拿到,這時就可以使用 Promise.allSettled(),他會返回陣列並包含所有的訊息,拿前面的相同例子來說:

let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2', false);
let p3 = createPromise(3, 'p3');

Promise.allSettled([p1, p2, p3])
  .then((res) => console.log(res))
  .catch((err) => console.log(err));
// [
// {status: 'fulfilled', value: 'p1'}, 
// {status: 'rejected', reason: 'err'},
// {status: 'fulfilled', value: 'p3'} 
// ]

Promise.race()

all() 相反 race() 是誰先執行完就回傳誰的結果
一樣來看例子

function createPromise(seconds, data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // console.log(data);
      resolve(data);
    }, seconds * 1000);
  });
}

let p1 = createPromise(1, 'p1');
let p2 = createPromise(2, 'p2');
let p3 = createPromise(3, 'p3');

Promise.race([p1, p2, p3]).then(res => console.log(res))
// 1 秒後回傳 p1

因為 p1 先完成就直接回傳 p1 的結果,後面的就也不會執行,也就不會管後面是失敗還成功。

Promise.prototype.finally()

finally() 是指 promise 結束後會執行的函式,有時候我們希望不管 promise 是成功還失敗都執行一些東西,就可以用到 finally()

function createPromise(seconds, data, success = true) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (success) {
        console.log(data);
        resolve();  
      }
      reject('err');
    }, seconds * 1000);
  });
}


let p1 = createPromise(1, 'p1');
p1.then(() => createPromise(1, 'p2'))
  .then(() => createPromise(1, 'p3'))
  .then(() => createPromise(1, 'p4'))
  .catch((err) => console.log(err))
  .finally(() => console.log('done'));

不管成功失敗,最後都會回傳 done。

Promise.then() 的第二個參數

其實 .then 最多可以傳入兩個函數當作參數 第一個是接收成功的資料,第二個接受失敗的資料。

.catch() 不同的是 .catch() 會跳過所有的 .then().then() 失敗之後還可以接其他的 .then() 繼續執行其它函數。

function onResolved(id) { 
 setTimeout(console.log, 0, id, 'resolved');
} 
function onRejected(id) { 
 setTimeout(console.log, 0, id, 'rejected'); 
} 
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000)); 
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000)); 

p1.then(() => onResolved('p1'), 
 () => onRejected('p1')); 
p2.then(() => onResolved('p2'), 
 () => onRejected('p2')); 

// 3秒後
// p1 resolved
// p2 rejected

Promise 觀念加強 - 不能 return 賦值給外層變數

在我剛學 Promise 的時候,我以為可以在 Promise 裡面 return 結果給一個變數,用 fetch 串接 API 取得資料當例子:

const result = fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then((response) => response.json())
  .then((json) => {
    return json;
  });

console.log(result) // 操作 result
// [object Promise] 
// {} 👈 結果會是空物件

如果你不知道甚麼是 fetch,可以先參考這篇文章

但這樣是不行的,因為就如同文章開頭所說 Promise 是一個對 還未有結果的事物 的一個替身,換句話說, Promise 代表了一個將來某一時刻才會知道結果的非同步操作。所以我們無法在直接將結果賦值給變數,因為 JS 執行到 fetch 那行程式碼時,我們仍不知道結果為何。

const result = fetch('https://jsonplaceholder.typicode.com/posts/1') 
  // 👆 我們不知道這時候的結果
  .then((response) => response.json())
  .then((json) => {
    return json;
  });

console.log(result) // 操作 result

所以當你在 Promise return 時時,你實際上返回的是一個 Promise 對象,而不是 Promise 內部 resolvereject 的值。

這也是未來 async / await 誕生的一個原因,這部分我們放在下篇說明。

小結

這樣 promise 就差不多講完了,今天提到的部分有

  1. 甚麼是 Promise
  2. 如何創建 Promise
  3. Promise 狀態與處理
  4. Promise 的其它用法
  5. Promise 觀念加強

希望你對 promise 有更了解一點,我覺得把 promise 當作一個替身的想法有幫助理解的,你覺得呢?

那今天就這樣,下篇貼文見~!

相關系列文章

JavaScript - Closure 閉包的詳解及實做應用前端必學 - 甚麼是 API?前端如何串接 API?

👉 前端重要觀念 Promise - 為甚麼要有 Promise?如何使用?