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