⏰ 限時 63 折優惠【React 效能優化實戰課】👉🏻 點我了解更多

前端基礎

-

前端重要觀念 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 出來後有顯示 fulfilled 和 rejected 嗎? 這就是成功和失敗的狀態,而 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 內部 resolve 或 reject 的值。

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

小結

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

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

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

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

你可能會感興趣的文章 👇