JavaScript ES6 Promise Object 物件

Promise 是一種非同步 (asynchronous) 編程的解決方案,所謂的 Promise,簡單來說它是一個等待非同步操作完成的物件,當事件完成時,Promise 根據操作結果是成功、或者失敗,做相對應的處理動作。

一個 Promise 物件 (只) 會處於下面三種狀態之一:

  1. pending - 初始狀態 (進行中)
  2. fulfilled - 事件已完成
  3. rejected - 事件已失敗

Promise 狀態的改變只有兩種可能:

  1. 從 pending 變成 fulfilled
  2. 從 pending 變成 rejected

而一但狀態改變就會固定,永遠不會再改變狀態了。

非同步最常見的例子像是 AJAX,傳統在處理非同步事件時會用一堆 nested callbacks,Promise 則是提供另外一種解決方案,讓你更直觀地控制非同步操作。

來看看建立一個 Promise 物件的語法:

var promise = new Promise(function(resolve, reject) {

  // ...
  
  if (異步操作成功) {
    resolve(value);
  } else {
    reject(error);
  }

});

Promise 構造函數 (constructor) 接受一個函數作為參數,這個函數會在建立 Promise 物件的同時立刻被執行,該函數有兩個參數分別是 resolve 函數和 reject 函數,resolve/reject 這兩個函數會由 JavaScript interpreter 自動傳入。

  • resolve(value) 函數的用途是用來將 Promise 物件的狀態變為 fulfilled (已完成),在非同步操作成功時調用,你可以將非同步操作的結果當作參數一起傳入
  • reject(error) 函數的用途是用來將 Promise 物件的狀態變為 rejected (已失敗),在非同步操作失敗時調用,你可以將非同步操作的錯誤當作參數一起傳入

此外,當 Promise constructor 的函數參數執行時,如果內部發生錯誤 throw error,Promise 物件的狀態會自動變成 rejected。

Promise.prototype.then(onFulfilled, onRejected)

Promise 物件生成後,可以用 then() 方法來綁定當 fulfilled 或 rejected 狀態時,分別要執行的函數。

promise.then(function(value) {
  // 當狀態是 fulfilled (成功) 時,執行這個函數
  // value 是透過 resolve() 傳進來的參數
  
}, function(error) {
  // 當狀態是 rejected (失敗) 時,執行這個函數
  // error 是透過 reject() 傳進來的參數

});

then() 方法接受兩個函數作為參數:

  • 第一個函數是當 Promise 狀態變為成功時會被調用
  • 第二個函數是當 Promise 狀態變為失敗時會被調用,這個參數是選擇性的不一定需要

舉一個 Promise 實際使用的例子:

let promise = new Promise(function(resolve, reject) {

  // 執行非同步的 setTimeout
  setTimeout(function(){
    // 250ms 過後,將 Promise 物件狀態改為成功
    resolve('Success!');
  }, 250);

});

promise.then(function(successMessage) {
  // 當 Promise 物件狀態變成功後執行這個函數
  
  console.log('Yay! ' + successMessage);
});

// 250ms 後你會看到輸出 "Yay! Success!"

Promise.prototype.catch(onRejected)

Promise 物件生成後,可以用 catch() 方法來綁定當 rejected 狀態時,要執行的函數。

catch() 的用途就像是 then(undefined, onRejected)。

var promise = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

promise.catch(function(e) {
  console.log(e);
});

// 顯示 "Uh-oh!"

Chaining 串接

then() 和 catch() 方法執行後都會返回一個新的 Promise 物件,讓你可以使用 chaining 的語法。

而後面的 then() 會接收前一個 then() 的 return value 當作參數。

如果 return value 的型態不是 Promise,會先執行 Promise.resolve()

例如:

var hello = new Promise(function(resolve, reject) {
  resolve('Hello');
});

hello.then(function(str) {
  return str + ' World';

}).then(function(str) {
  return str;

}).then(function(str) {
  console.log(str);
});

// 最後輸出 "Hello World"

Promise.all(iterable)

Promise.all() 函數用來將多個 Promise 物件包裝成一個 Promise 物件,他接受的參數可以是一個陣列,陣列中放不同的 Promise 物件。

如果陣列中有元素的型態不是 Promise,會先執行 Promise.resolve()

而新的 Promise 物件的狀態會怎麼改變?

  1. 狀態變為 fulfilled: 如果它包含的所有 Promise 物件狀態都變為 fulfilled。而所有 Promise 物件個別的返回值,會被組成一個陣列傳進 all Promise 物件的 callback
  2. 狀態變為 rejected: 如果它包含的其中一個 Promise 物件狀態變為 rejected。而第一個被 reject 的值會被傳進回 all Promise 物件的 callback
var p1 = Promise.resolve(3);

var p2 = 1337;

var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, 'foo');
}); 

Promise.all([p1, p2, p3]).then(function(values) { 
  console.log(values);
});

// 會顯示 [3, 1337, "foo"]

Promise.race(iterable)

Promise.race() 函數和 Promise.all() 一樣用來將多個 Promise 物件包裝成一個 Promise 物件。

不同的地方在於,只要它包含的所有 Promise 物件其中任何一個的狀態先改變,race Promise 物件的狀態就會跟著改變,率先改變狀態的 Promise 物件參數會直接傳給 race Promise 物件的 callback。

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, 'one'); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, 'two'); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value);
});

// 會顯示 "two",因為 p2 比較快被 resolve

Promise.resolve(value)

Promise.resolve() 函數用來將一個物件轉型為 Promise (如果它不是一個 Promise 物件),然後立刻 resolve 它。

Promise.resolve('Success').then(function(value) {
  console.log(value);
}, function(value) {
  console.log('Fail');
});

// 輸出 "Success"

Promise.reject(reason)

Promise.reject() 函數用來將一個物件轉型為 Promise (如果它不是一個 Promise 物件),然後立刻 reject 它。

Promise.reject(new Error('Fail')).then(function(error) {
  console.log('Success');
}, function(error) {
  console.log('Fail');
});

// 輸出 "Fail"