本章將主要對 JavaScript 中的 Promise 進行入門級的介紹。
首先讓我們來了解一下到底什么是 Promise。
Promise 是抽象異步處理對象以及對其進行各種操作的組件。 其詳細內(nèi)容在接下來我們還會進行介紹,Promise 并不是從 JavaScript 中發(fā)祥的概念。
Promise 最初被提出是在 E 語言中, 它是基于并列/并行處理設(shè)計的一種編程語言。
現(xiàn)在 JavaScript 也擁有了這種特性,這就是本書所介紹的 JavaScript Promise。
另外,如果說到基于 JavaScript 的異步處理,我想大多數(shù)都會想到利用回調(diào)函數(shù)。
使用了回調(diào)函數(shù)的異步處理
----
getAsync("fileA.txt", function(error, result){
if(error){// 取得失敗時的處理
throw error;
}
// 取得成功時的處理
});
----
<1> 傳給回調(diào)函數(shù)的參數(shù)為(error對象, 執(zhí)行結(jié)果)組合
Node.js 等則規(guī)定在 JavaScript 的回調(diào)函數(shù)的第一個參數(shù)為 Error 對象,這也是它的一個慣例。
像上面這樣基于回調(diào)函數(shù)的異步處理如果統(tǒng)一參數(shù)使用規(guī)則的話,寫法也會很明了。 但是,這也僅是編碼規(guī)約而已,即使采用不同的寫法也不會出錯。
而 Promise 則是把類似的異步處理對象和處理規(guī)則進行規(guī)范化, 并按照采用統(tǒng)一的接口來編寫,而采取規(guī)定方法之外的寫法都會出錯。
下面是使用了Promise進行異步處理的一個例子
----
var promise = getAsyncPromise("fileA.txt"); (1)
promise.then(function(result){
// 獲取文件內(nèi)容成功時的處理
}).catch(function(error){
// 獲取文件內(nèi)容失敗時的處理
});
----
<1> 返回promise對象
我們可以向這個預(yù)設(shè)了抽象化異步處理的 promise 對象, 注冊這個 promise 對象執(zhí)行成功時和失敗時相應(yīng)的回調(diào)函數(shù)。
這和回調(diào)函數(shù)方式相比有哪些不同之處呢? 在使用 promise 進行一步處理的時候,我們必須按照接口規(guī)定的方法編寫處理代碼。
也就是說,除 promise 對象規(guī)定的方法(這里的 then
或 catch
)以外的方法都是不可以使用的, 而不會像回調(diào)函數(shù)方式那樣可以自己自由的定義回調(diào)函數(shù)的參數(shù),而必須嚴格遵守固定、統(tǒng)一的編程方式來編寫代碼。
這樣,基于 Promise 的統(tǒng)一接口的做法, 就可以形成基于接口的各種各樣的異步處理模式。
所以,promise 的功能是可以將復(fù)雜的異步處理輕松地進行模式化, 這也可以說得上是使用 promise 的理由之一。
接下來,讓我們在實踐中來學(xué)習(xí) JavaScript 的 Promise 吧。
在 ES6 Promises 標準中定義的 API 還不是很多。
目前大致有下面三種類型。
Constructor
Promise 類似于 XMLHttpRequest
,從構(gòu)造函數(shù) Promise
來創(chuàng)建一個新建新 promise
對象作為接口。
要想創(chuàng)建一個 promise 對象、可以使用 new
來調(diào)用 Promise
的構(gòu)造器來進行實例化。
var promise = new Promise(function(resolve, reject) {
// 異步處理
// 處理結(jié)束后、調(diào)用resolve 或 reject
});
Instance Method
對通過 new 生成的 promise 對象為了設(shè)置其值在 resolve(成功) / reject(失敗)時調(diào)用的回調(diào)函數(shù) 可以使用 promise.then()
實例方法。
promise.then(onFulfilled, onRejected)
resolve(成功)時
onFulfilled
會被調(diào)用
reject(失敗)時
onRejected
會被調(diào)用
onFulfilled
、onRejected
兩個都為可選參數(shù)。
promise.then
成功和失敗時都可以使用。 另外在只想對異常進行處理時可以采用 promise.then(undefined, onRejected)
這種方式,只指定 reject 時的回調(diào)函數(shù)即可。 不過這種情況下 promise.catch(onRejected)
應(yīng)該是個更好的選擇。
promise.catch(onRejected)
Static Method
像 Promise
這樣的全局對象還擁有一些靜態(tài)方法。
包括 Promise.all()
還有 Promise.resolve()
等在內(nèi),主要都是一些對 Promise 進行操作的輔助方法。
我們先來看一看下面的示例代碼。
function asyncFunction() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('Async Hello world');
}, 16);
});
}
asyncFunction().then(function (value) {
console.log(value); // => 'Async Hello world'
}).catch(function (error) {
console.log(error);
});
asyncFunction
這個函數(shù)會返回 promise 對象,對于這個 promise 對象,我們調(diào)用它的 then
方法來設(shè)置resolve后的回調(diào)函數(shù),catch
方法來設(shè)置發(fā)生錯誤時的回調(diào)函數(shù)。
該 promise 對象會在 setTimeout 之后的 16 ms 時被 resolve,
這時 then
的回調(diào)函數(shù)會被調(diào)用,并輸出 'Async Hello world'
。
在這種情況下 catch
的回調(diào)函數(shù)并不會被執(zhí)行(因為 promise 返回了 resolve),不過如果運行環(huán)境沒有提供 setTimeout
函數(shù)的話,那么上面代碼在執(zhí)行中就會產(chǎn)生異常,在 catch
中設(shè)置的回調(diào)函數(shù)就會被執(zhí)行。
當(dāng)然,像promise.then(onFulfilled, onRejected)
的方法聲明一樣,
如果不使用catch
方法只使用 then
方法的話,如下所示的代碼也能完成相同的工作。
asyncFunction().then(function (value) {
console.log(value);
}, function (error) {
console.log(error);
});
我們已經(jīng)大概了解了 Promise 的處理流程,接下來讓我們來稍微整理一下 Promise 的狀態(tài)。
用 new Promise
實例化的 promise 對象有以下三個狀態(tài)。
"has-resolution" - Fulfilled::
resolve(成功)時。此時會調(diào)用 onFulfilled
"has-rejection" - Rejected::
reject(失敗)時。此時會調(diào)用 onRejected
"unresolved" - Pending::
既不是 resolve 也不是 reject 的狀態(tài)。也就是 promise 對象剛被創(chuàng)建后的初始化狀態(tài)等
關(guān)于上面這三種狀態(tài)的讀法,其中 左側(cè)為在 ES6 Promises 規(guī)范中定義的術(shù)語, 而右側(cè)則是在 Promises/A+ 中描述狀態(tài)的術(shù)語。
基本上狀態(tài)在代碼中是不會涉及到的,所以名稱也無需太在意。 在這本書中,我們會基于 Promises/A+](v)
中 Pending 、 Fulfilled 、 Rejected 的狀態(tài)名稱進行講述。
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/1.1.png" alt="picture1.1" />
Figure 1. promise states
在 ECMAScript Language Specification ECMA-262 6th Edition – DRAFT 中
[[PromiseStatus]]
都是在內(nèi)部定義的狀態(tài)。 由于沒有公開的訪問[[PromiseStatus]]
的用戶 API,所以暫時還沒有查詢其內(nèi)部狀態(tài)的方法。
到此在本文中我們已經(jīng)介紹了 promise 所有的三種狀態(tài)。
promise 對象的狀態(tài),從 Pending 轉(zhuǎn)換為 Fulfilled 或 Rejected 之后,這個 promise 對象的狀態(tài)就不會再發(fā)生任何變化。
也就是說,Promise 與 Event 等不同,在 .then
后執(zhí)行的函數(shù)可以肯定地說只會被調(diào)用一次。
另外,Fulfilled 和 Rejected 這兩個中的任一狀態(tài)都可以表示為 Settled(不變的)。
Settled::
resolve(成功) 或 reject(失敗)。
從Pending和Settled的對稱關(guān)系來看,Promise 狀態(tài)的種類/遷移是非常簡單易懂的。
當(dāng) promise 的對象狀態(tài)發(fā)生變化時,用 .then
來定義只會被調(diào)用一次的函數(shù)。
JavaScript Promises - Thinking Sync in an Async World // Speaker Deck 這個 ppt 中有關(guān)于 Promise 狀態(tài)遷移的非常容易理解的說明。
這里我們來介紹一下如何編寫Promise代碼。
創(chuàng)建 promise 對象的流程如下所示。
new Promise(fn)
返回一個promise對象fn
中指定異步等處理
resolve(處理結(jié)果值)
reject(Error對象)
按這個流程我們來實際編寫下 promise 代碼吧。
我們的任務(wù)是用 Promise 來通過異步處理方式來獲取 XMLHttpRequest(XHR) 的數(shù)據(jù)。
創(chuàng)建 XHR 的 promise 對象
首先,創(chuàng)建一個用 Promise 把 XHR 處理包裝起來的名為 getURL 的函數(shù)
function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}
getURL
只有在通過 XHR 取得結(jié)果狀態(tài)為 200 時才會調(diào)用 resolve
- 也就是只有數(shù)據(jù)取得成功時,而其他情況(取得失?。r則會調(diào)用 reject
方法。
resolve(req.responseText)
在 response 的內(nèi)容中加入了參數(shù)。resolve 方法的參數(shù)并沒有特別的規(guī)則,基本上把要傳給回調(diào)函數(shù)參數(shù)放進去就可以了。( then
方法可以接收到這個參數(shù)值)
熟悉 Node.js 的人,經(jīng)常會在寫回調(diào)函數(shù)時將 callback(error, response)
的第一個參數(shù)設(shè)為 error 對象,而在 Promise 中 resolve/reject 則擔(dān)當(dāng)了這個職責(zé)(處理正常和異常的情況),所以在 resolve 方法中只傳一個 response 參數(shù)是沒有問題的。
接下來我們來看一下 reject
函數(shù)。
XHR 中 onerror
事件被觸發(fā)的時候就是發(fā)生錯誤時,所以理所當(dāng)然調(diào)用 reject
。
這里我們重點來看一下傳給 reject
的值。
發(fā)生錯誤時要像這樣 reject(new Error(req.statusText));
,創(chuàng)建一個 Error 對象后再將具體的值傳進去。傳給 reject
的參數(shù)也沒有什么特殊的限制,一般只要是 Error 對象(或者繼承自 Error 對象)就可以。
傳給 reject
的參數(shù),其中一般是包含了 reject 原因的 Error 對象。本次因為狀態(tài)值不等于 200 而被 reject,所以 reject
中放入的是 statusText。(這個參數(shù)的值可以被 then
方法的第二個參數(shù)或者 catch
方法中使用)
讓我們在實際中使用一下剛才創(chuàng)建的返回 promise 對象的函數(shù)
getURL("http://example.com/"); // => 返回promise對象
如 Promises Overview 中做的簡單介紹一樣,promise 對象擁有幾個實例方法, 我們使用這些實例方法來為 promise 對象創(chuàng)建依賴于 promise 的具體狀態(tài)、并且只會被執(zhí)行一次的回調(diào)函數(shù)。
為 promise 對象添加處理方法主要有以下兩種
http://wiki.jikexueyuan.com/project/javascript-promise-mini-book/images/1.2.png" alt="picture1.2" />
Figure 2. promise value flow
首先,我們來嘗試一下為 getURL
通信成功并取到值時添加的處理函數(shù)。
此時所謂的 通信成功 ,指的就是在被 resolve 后, promise 對象變?yōu)?FulFilled 狀態(tài) 。
被 resolve 后的處理,可以在 .then
方法中傳入想要調(diào)用的函數(shù)。
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
console.log(value);
});
<1>
為了方便理解我們把函數(shù)命名為 onFulfilled
getURL函數(shù) 中的 resolve(req.responseText);
會將 promise 對象變?yōu)?resolve(Fulfilled)狀態(tài),
同時使用其值調(diào)用 onFulfilled
函數(shù)。
不過目前我們還沒有對其中可能發(fā)生的錯誤做任何處理,接下來,我們就來為 getURL
函數(shù)添加發(fā)生錯誤時的異常處理。
此時 發(fā)生錯誤 ,指的也就是 reject 后 promise 對象變?yōu)?Rejected 狀態(tài) 。
被 reject 后的處理,可以在.then
的第二個參數(shù)或者是在 .catch
方法中設(shè)置想要調(diào)用的函數(shù)。
把下面 reject 時的處理加入到剛才的代碼,如下所示。
var URL = "http://httpbin.org/status/500";
getURL(URL).then(function onFulfilled(value){
console.log(value);
}).catch(function onRejected(error){
console.error(error);
});
<1>
服務(wù)端返回的狀態(tài)碼為 500
<2>
為了方便理解函數(shù)被命名為 onRejected
在 getURL
的處理中發(fā)生任何異常,或者被明確 reject 的情況下,
該異常原因(Error 對象)會作為 .catch
方法的參數(shù)被調(diào)用。
其實 .catch 的別名而已, 如下代碼也可以完成同樣的功能。)只是 promise.then(undefined, onRejected)
的別名而已, 如下代碼也可以完成同樣的功能。
getURL(URL).then(onFulfilled, onRejected);<1>
<1>
onFulfilled, onRejected 是和剛才相同的函數(shù)
一般說來,使用.catch來將 resolve 和 reject 處理分開來寫是比較推薦的做法, 這兩者的區(qū)別會在 then 和 catch 的區(qū)別中再做詳細介紹。
總結(jié)
在本章我們簡單介紹了以下內(nèi)容:
new Promise
方法創(chuàng)建 promise 對象.then
或 .catch
添加 promise 對象的處理函數(shù)到此為止我們已經(jīng)學(xué)習(xí)了 Promise 的基本寫法。 其他很多處理都是由此基本語法延伸的,也使用了 Promise 提供的一些靜態(tài)方法來實現(xiàn)。
實際上即使使用回調(diào)方式的寫法也能完成上面同樣的工作,而使用 Promise 方式的話有什么優(yōu)點么?在本小節(jié)中我們沒有講到兩者的對比及 Promise 的優(yōu)點。在接下來的章節(jié)中,我們將會對 Promise 優(yōu)點之一,即錯誤處理機制進行介紹,以及和傳統(tǒng)的回調(diào)方式的對比。