0%

JS特訓-DAY40-ES6 團戰關卡 Promise (2)

前言

本文將專注於解釋 Promise 的意義以及用法

什麼是 Promise

  • ES6 新增的建構函式,用來優化非同步的語法
  • 解決 AJAX 的非同步問題,同時增加可讀性
  • 常見例如 Axios、Fetch

為什麼需要 Promise

  • JavaScript 是屬於同步的程式語言,因此一次僅能做一件事情
  • 遇到非同步的事件時,就會將非同步的事件移動到程式碼的最後方,等到所有的原始碼運行完以後才會執行非同步的事件

以下列的程式碼來說,在 console 中依序的會出現的順序為:

開始 → 程式碼結束 → 非同步事件

console.log('開始');

setTimeout(() => {
  console.log('非同步事件');
}, 0);

console.log('程式碼結束');

原始碼中,setTimeout 所定義的時間為 0,但因為是屬於非同步事件,因此還是會在其他原始碼運行完以後才執行,而 Ajax 也是屬於非同步行為,當需要確保擷取到遠端資料才繼續往下執行時,如果程式碼是依序撰寫的方式,就會無法正確呈現資料


Promise 的意義

Promise,如字面的意思就代表承諾。


承諾的未來

當你拿到一個 Promise 的時候,代表在未來中這個 Promise 可能會有幾種狀況發生

  • 承諾 被兌現 (fulfilled)

resolve( ) 來兌現

  • 承諾 被打破 (rejected)

reject( ) 來表示失敗

  • 承諾 一直沒有回應 (pending)

一直沒有回傳

也就是說,承諾代表的不見得是成功。


未來之後

根據這三種結果,我們接下來的動作會有所不同,如果:

  • 承諾被兌現 就 繼續做預定好的下一件事

使用 .then( )

  • 承諾被打破 就 根據這個原因去做對應的動作

使用 .catch( ),或是 .then 的第二個參數

  • 承諾 一直都沒有回應 就 繼續等下去


Promise 用法

首先要先了解到底要怎麼創建一個 Promise,很簡單,只要 new 一個 Promise 物件出來即可:

var a = new Promise(function(resolve, reject) {
    // sync or async codes
      // if success, resolve
      // if fail, reject
    });

Promise 可以帶入一個函式,代表著要給予承諾的程式。

這個函式會被 Promise 傳入兩個參數,這兩個參數也是函式,他們分別代表:

  • 兌現 (fulfilled)

通常以 resolve 命名該參數
當我們完成動作時,就呼叫 resolve( ) 來兌現我們的承諾

  • 拒絕 (rejected)

通常以 reject 命名該參數
當我們動作失敗時,就呼叫 reject() 來打破我們的承諾


Promise 被兌現 (fulfilled)

讓我們看看要怎麼兌現承諾,以及兌現之後要怎麼接續動作:

var a = new Promise(function(resolve, reject) {
      setTimeout(function(){
        resolve('hello world');
      }, 1000);
      });

a.then(function(value) {
  console.log(a);            // Promise {<resolved>: "hello world"}
  console.log(value + '1');  // "hello world1"
});
a.then(function(value) {
  console.log(a);            // Promise {<resolved>: "hello world"}
  console.log(value + '2');  // "hello world2"
});

console.log(a);              // Promise {<pending>}

先來看我們非同步的函式 setTimeout 這邊。很清楚的可以看到,這個非同步會在 1000 毫秒後 兌現承諾 ( 因為呼叫了

resolve( ) )。




那等待兌現承諾著這一秒期間發生了什麼事情呢?讓我們往後面的程式碼看。我們緊接著會看到兩句 a.then,還記得前面說過

的,.then( ) 是代表承諾被實現之後才會去執行的,因此在這一秒內,我們都不會看到 a.then 會印出東西。




接著我們在看最後一句:console.log(a),這句並沒有被 then 所包住,因此這並不會等待 Promise 完成後才執行,就像

等餐的時候我們還是可以滑手機一樣,console.log(a) 這句就代表滑手機這個動作,因為它與 Promise 對先與否不相關,所以

可以先做。此時印出 a 會發現他的值是 "Promise {<pending>}"這就代表承諾還沒有回應,等待中




一秒到了,a.then 就會緊接著被執行。可以注意到的是,a 的值是可以保留下來的,因此無論你接了幾個 then,他都可以偵

測到 a 這個承諾的完成狀況。因此我們接著就會印出 Promise {<resolved>: "hello world1"}"hello world1"

Promise {<resolved>: "hello world2"}"hello world2" 兩組。可以看到此時 a 的值變成了

Promise {<resolved>: "hello world"},這代表此承諾已經被兌現,而兌現後他會給我們 "hello world" 這個值。


Promise 被拒絕 (rejected)

var a = new Promise(function(resolve, reject) {
        setTimeout(function(){
          reject('OOPS');
        }, 1000);
      });

a.catch(function(value) {
  console.log(a);       // Promise {<reject>: "OOPS"}
  console.log(value);   // "OOPS"
});

我們的非同步程式邏輯不變,只把 resolve 改成 reject

之後,我們使用 .catch 來抓住錯誤。過了一秒後,我們就可以得到 Promise 拒絕承諾的訊息了


Promise 一直沒有回應 (pending)

當我們並沒有成功 resolve 或是 reject 的時候,就會 pending,也就是說 .then 內的程式會一直等待,而 .catch 也不會抓到任何錯誤。

var a = new Promise(function(resolve, reject) {});

console.log(a);       // Promise {<pending>}

雖然這些例子都有點刻意,但是卻都很容易出現在日常中。像是當我們試圖去連到一個網站,而他卻一直沒有回應時,我們的 Promise 就會一直處於 pending 狀態。


Promise Chain

當你可以等待一個非同步動作完成後,再去執行下一個動作時,那接著你可能就會想說,如果有很多非同步動作要串接在一起的

話要怎麼辦呢?

很幸運的,.then 可以串接非同步程式。講得更精確一點是,.then 不論是非同步或者同步的程式都可以串接,而 then 會回

傳一個 Promise。既然他又會回傳一個 Promise 就代表我們可以串接非同步程式。

也許你會很疑惑的問,假如我們的 .then 執行的是一個同步程式的話,那他還會回傳 Promise 物件嗎 ( 畢竟非同步才需要

承諾 )?答案是會,.then 會把我們的 return 值包裹成一個 Promise,並回傳回去。

讓我們來看看範例:

asyncFn()
.then(syncFn)
.then(asyncFn);

function asyncFn(data) {
  return new Promise(function(resolve, reject) {
            console.log('Async received data:', data);
        setTimeout(function(){
        resolve('async fn');
        }, 1000);
        });
}

function syncFn(data) {
  console.log('Sync received data:', data);
  return 'sync fn';
}

// "Async received data: undefined"
// "Sync received data: async fn"
// "Async received data: sync fn"

在這裡,我們先使用 asyncFn 回傳一個 Promise 回去。一秒後,syncFn 收到了來自 asyncFn 的兌現的承諾值 "async fn"

在印出 "Sync received data: async fn" 之後,便 return 'sync fn'。接著,JavaScript 會把 'sync fn' 包裹成一個

Promise,因此我們可以再繼續 Chain 到下一個 .then 執行 asyncFn

這就證明了不論是非同步或者同步的程式都可以串接,而 then 會回傳一個 Promise 好讓我們繼續串接。


Promise 方法

最後,介紹 Promise 中其它的方法,就 Promise 的物件下,展開後可以看到以下方法:

Promise.all

  • 透過陣列的形式傳入多個 promise 函式
  • 在全部執行完成後回傳陣列結果
  • 陣列的結果順序與一開始傳入的一致
  • 適合用在多支 API 要一起執行,並確保全部完成後才進行其他工作
Promise.all([promise(1), promise(2), promise(3)])
    .then(res => {
        console.log(res) // ["1", "2", "3"]
})

Promise.race

  • 多個 Promise 同時執行,但僅回傳第一個完成的結果
Promise.race([promise(1), promise(2), promise(3)])
    .then(res => {
        console.log(res) // "1"
})

Promise.reject、Promise.resolve

  • 這兩個方法是直接定義 Promise 物件已經完成的狀態(resolve, reject
  • new Promise 一樣會產生一個新的 Promise 物件,但結果是已經確定的

以下提供範例說明:

使用 Promise.resolve 產生一個新的 Promise 物件,此物件可以使用 then 取得 resolve 的結果。

var result = Promise.resolve('result');
result.then(res => {
  console.log('resolved', res); // 成功部分可以正確接收結果
}, res => {
  console.log('rejected', res); // 失敗部分不會取得結果
});

改為 Promise.reject 產生 Promise 物件,此物件必定呈現 rejected 的結果。

var result = Promise.reject('result');
result.then(res => {
  console.log(res);
}, res => {
  console.log(res); // 只有此段會出現結果
});
// rejected result

注意:Promise.rejectPromise.resolve 是直接定義結果,無論傳入的是否為 Promise 物件。


參考