鍍金池/ 教程/ HTML/ 什么是 Promise
Promise 測試
實戰(zhàn) Promise
用語集
前言
什么是 Promise
API Reference
Advanced

什么是 Promise

本章將主要對 JavaScript 中的 Promise 進行入門級的介紹。

1.1. 什么是 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ī)定的方法(這里的 thencatch)以外的方法都是不可以使用的, 而不會像回調(diào)函數(shù)方式那樣可以自己自由的定義回調(diào)函數(shù)的參數(shù),而必須嚴格遵守固定、統(tǒng)一的編程方式來編寫代碼。

這樣,基于 Promise 的統(tǒng)一接口的做法, 就可以形成基于接口的各種各樣的異步處理模式。

所以,promise 的功能是可以將復(fù)雜的異步處理輕松地進行模式化, 這也可以說得上是使用 promise 的理由之一。

接下來,讓我們在實踐中來學(xué)習(xí) JavaScript 的 Promise 吧。

1.2. 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 進行操作的輔助方法。

1.2.1. Promise workflow

我們先來看一看下面的示例代碼。

    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);
    });

1.2.2. Promise 的狀態(tài)

我們已經(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)換為 FulfilledRejected 之后,這個 promise 對象的狀態(tài)就不會再發(fā)生任何變化。

也就是說,Promise 與 Event 等不同,在 .then 后執(zhí)行的函數(shù)可以肯定地說只會被調(diào)用一次。

另外,FulfilledRejected 這兩個中的任一狀態(tài)都可以表示為 Settled(不變的)。

Settled::
resolve(成功) 或 reject(失敗)。

PendingSettled的對稱關(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)遷移的非常容易理解的說明。

1.3. 編寫 Promise 代碼

這里我們來介紹一下如何編寫Promise代碼。

1.3.1. 創(chuàng)建 promise 對象

創(chuàng)建 promise 對象的流程如下所示。

  • new Promise(fn) 返回一個promise對象
  • fn 中指定異步等處理
    • 處理結(jié)果正常的話,調(diào)用 resolve(處理結(jié)果值)
    • 處理結(jié)果錯誤的話,調(diào)用 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 方法中使用)

1.3.2. 編寫 promise 對象處理方法

讓我們在實際中使用一下剛才創(chuàng)建的返回 promise 對象的函數(shù)

    getURL("http://example.com/"); // => 返回promise對象 

Promises Overview 中做的簡單介紹一樣,promise 對象擁有幾個實例方法, 我們使用這些實例方法來為 promise 對象創(chuàng)建依賴于 promise 的具體狀態(tài)、并且只會被執(zhí)行一次的回調(diào)函數(shù)。

為 promise 對象添加處理方法主要有以下兩種

  • promise對象被 resolve 時的處理(onFulfilled)
  • promise對象被 reject 時的處理(onRejected)

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)方式的對比。

上一篇:實戰(zhàn) Promise下一篇:Promise 測試