Promise和異步編程

前面的話

  JS有不少強大的功能,其中一個是它能夠輕鬆地搞定異步編程。做爲一門爲Web而生的語言,它從一開始就須要可以響應異步的用戶交互,如點擊和按鍵操做等。Node.js用回調函數代替了事件,使異步編程在JS領域更加流行。但當更多程序開始使用異步編程時,事件和回調函數卻不能知足開發者想要作的全部事情,它們還不夠強大,而Promise就是這些問題的解決方案編程

  Promise能夠實現其餘語言中相似Future和Deferred同樣的功能,是另外一種異步編程的選擇,它既能夠像事件和回調函數同樣指定稍後執行的代碼,也能夠明確指示代碼是否成功執行。基於這些成功或失敗的狀態,爲了讓代碼更容易理解和調試,能夠鏈式地編寫Promise。本文將詳細介紹Promise和異步編程json

 

引入

  JS引擎是基於單線程(Single-threaded)事件循環的概念構建的,同一時刻只容許一個代碼塊在執行,與之相反的是像Java和C++同樣的語言,它們容許多個不一樣的代碼塊同時執行。對於基於線程的軟件而言,當多個代碼塊同時訪問並改變狀態時,程序很難維護並保證狀態不會出錯 數組

  JS引擎同一時刻只能執行一個代碼塊,因此須要跟蹤即將運行的代碼,那些代碼被放在一個任務隊列(job queue)中,每當一段代碼準備執行時,都會被添加到任務隊列。每當JS引擎中的一段代碼結束執行,事件循環(event toop)會執行隊列中的下一個任務,它是JS引擎中的一段程序,負責監控代碼執行並管理任務隊列。隊列中的任務會從第一個一直執行到最後一個promise

【事件模型】瀏覽器

  用戶點擊按鈕或按下鍵盤上的按鍵會觸發相似onclick這樣的事件,它會向任務隊列添加一個新任務來響應用戶的操做,這是JS中最基礎的異步編程形式,直到事件觸發時才執行事件處理程序,且執行時上下文與定義時的相同異步

let button = document.getElementById("my-btn");
button.onclick = function(event) {
    console.log("Clicked");
};

  在這段代碼中,單擊button後會執行console.log("clicked"),賦值給onclick的函數被添加到任務隊列中,只有當前面的任務都完成後它纔會被執行async

  事件模型適用於處理簡單的交互,然而將多個獨立的異步調用鏈接在一塊兒會使程序更加複雜,由於必須跟蹤每一個事件的事件目標(如此示例中的button)。此外,必需要保證事件在添加事件處理程序以後才被觸發。例如,若是先單擊button再給onclick賦值,則任何事情都不會發生。因此,儘管事件模型適用於響應用戶交互和完成相似的低頻功能,但其對於更復雜的需求來講卻不是很靈活異步編程

【回調模式】函數

  Node.js經過普及回調函數來改進異步編程模型,回調模式與事件模型相似,異步代碼都會在將來的某個時間點執行,兩者的區別是回調模式中被調用的函數是做爲參數傳入的工具

複製代碼
readFile("example.txt", function(err, contents) {
    if (err) {
        throw err;
    }
    console.log(contents);
});
console.log("Hi!");
複製代碼

  此示例使用Node.js傳統的錯誤優先(error-first)回調風格。readFile()函數讀取磁盤上的某個文件(指定爲第一個參數),讀取結束後執行回調函數(第二個參數)。若是出現錯誤,錯誤對象會被賦值給回調函數的err參數;若是一切正常,文件內容會以字符串的形式被賦值給contents參數

  因爲使用了回調模式,readFile()函數當即開始執行,當讀取磁盤上的文件時會暫停執行。也就是說,調用readFile()函數後,console.log("Hi")語句當即執行並輸出"Hi";當readFile()結束執行時,會向任務隊列的末尾添加一個新任務,該任務包含回調函數及相應的參數,當隊列前面全部的任務完成後才執行該任務,並最終執行console.log(contents)輸出全部內容

  回調模式比事件模型更靈活,由於相比之下,經過回調模式連接多個調用更容易

複製代碼
readFile("example.txt", function(err, contents) {
    if (err) {
        throw err;
    }
    writeFile("example.txt", function(err) {
        if (err) {
            throw err;
        }
        console.log("File was written!");
    });
});
複製代碼

  在這段代碼中,成功調用readFile()函數後會執行writeFile()函數的異步調用。在這兩個函數中是經過相同的基本模式來檢查err是否存在的。當readFile()函數執行完成後,會向任務隊列中添加一個任務,若是沒有錯誤產生,則執行writeFile()函數,而後當writeFile()函數執行結束後也向任務隊列中添加一個任務

  雖然這個模式運行效果很不錯,但若是嵌套了太多的回調函數,極可能會陷入回調地獄

複製代碼
method1(function(err, result) {
    if (err) {
        throw err;
    }
    method2(function(err, result) {
        if (err) {
            throw err;
        }
        method3(function(err, result) {
            if (err) {
                throw err;
            }
            method4(function(err, result) {
                if (err) {
                    throw err;
                }
                method5(result);
            });
        });
    });
});
複製代碼

  像示例中這樣嵌套多個方法調用,會建立出一堆難以理解和調試的代碼。若是想實現更復雜的功能,回調函數的侷限性一樣也會顯現出來。例如,並行執行兩個異步操做,當兩個操做都結束時通知你;或者同時進行兩個異步操做,只取優先完成的操做結果。在這些狀況下,須要跟蹤多個回調函數並清理這些操做,而Promise就能很是好地改進這樣的狀況

 

基礎

  Promise至關於異步操做結果的佔位符,它不會去訂閱一個事件,也不會傳遞一個回調函數給目標函數,而是讓函數返回一個Promise

// readFile 承諾會在未來某個時間點完成
let promise = readFile("example.txt");

  在這段代碼中,readFile()不會當即開始讀取文件,函數會先返回一個表示異步讀取操做的Promise對象,將來對這個對象的操做徹底取決於Promise的生命週期

【Promise的生命週期】

  每一個Promise都會經歷一個短暫的生命週期:先是處於進行中(pending)的狀態,此時操做還沒有完成,因此它也是未處理(unsettled)的;一旦異步操做執行結束,Promise則變爲已處理(settled)的狀態

  在以前的示例中,當readFile()函數返回promise時它變爲pending狀態,操做結束後,Promise可能會進入到如下兩個狀態中的其中一個

  一、Fulfilled

  Promise異步操做成功完成

  二、Rejected

  因爲程序錯誤或一些其餘緣由,Promise異步操做未能成功

  內部屬性[[PromiseState]]被用來表示Promise的3種狀態:"pending"、"fulfilled"及"rejected"。這個屬性不暴露在Promise對象上,因此不能以編程的方式檢測Promise的狀態,只有當Promise的狀態改變時,經過then()方法來採起特定的行動

  全部Promise都有then()方法,它接受兩個參數:第一個是當Promise的狀態變爲fulfilled時要調用的函數,與異步操做相關的附加數據都會傳遞給這個完成函數(fulfillment function);第二個是當Promise的狀態變爲rejected時要調用的函數,其與完成時調用的函數相似,全部與失敗狀態相關的附加數據都會傳遞給這個拒絕函數(rejection function)

  [注意]若是一個對象實現了上述的then()方法,那這個對象咱們稱之爲thenable對象。全部的Promise都是thenable對象,但並不是全部thenable對象都是Promise

  then()的兩個參數都是可選的,因此能夠按照任意組合的方式來監聽Promise,執行完成或被拒絕都會被響應

複製代碼
let promise = readFile("example.txt");
promise.then(function(contents) {
    // 完成
    console.log(contents);
}, function(err) {
    // 拒絕
    console.error(err.message);
});
promise.then(function(contents) {
    // 完成
    console.log(contents);
});
promise.then(null, function(err) {
    // 拒絕
    console.error(err.message);
});
複製代碼

  上面這3次then()調用操做的是同一個Promise。第一個同時監聽了執行完成和執行被拒;第二個只監聽了執行完成,錯誤時不報告;第三個只監聽了執行被拒,成功時不報告

  Promise還有一個catch()方法,至關於只給其傳入拒絕處理程序的then()方法

複製代碼
promise.catch(function(err) {
    // 拒絕
    console.error(err.message);
});
// 等同於:
promise.then(null, function(err) {
    // 拒絕
    console.error(err.message);
});
複製代碼

  then()方法和catch()方法一塊兒使用才能更好地處理異步操做結果。這套體系可以清楚地指明操做結果是成功仍是失敗,比事件和回調函數更好用。若是使用事件,在遇到錯誤時不會主動觸發;若是使用回調函數,則必需要記得每次都檢查錯誤參數。若是不給Promise添加拒絕處理程序,那全部失敗就自動被忽略了,因此必定要添加拒絕處理程序,即便只在函數內部記錄失敗的結果也行

  若是一個Promise處於己處理狀態,在這以後添加到任務隊列中的處理程序仍將執行。因此不管什麼時候均可以添加新的完成處理程序或拒絕處理程序,同時也能夠保證這些處理程序能被調用

複製代碼
let promise = readFile("example.txt");
    // 原始的完成處理函數
    promise.then(function(contents) {
        console.log(contents);
        // 如今添加另外一個
        promise.then(function(contents) {
            console.log(contents);
        });
});
複製代碼

  在這段代碼中,一個完成處理程序被調用時向同一個Promise添加了另外一個完成處理程序,此時這個Promise已完成,因此新的處理程序會被添加到任務隊列中,前面的任務完成後其才被調用。這對拒絕處理程序也一樣適用

  [注意]每次調用then()方法或catch()方法都會建立一個新任務,當Promise被解決(resolved)時執行。這些任務最終會被加入到一個爲Promise量身定製的獨立隊列中

【建立未完成的Promise】

  用Promise構造函數能夠建立新的Promise,構造函數只接受一個參數:包含初始化Promise代碼的執行器(executor)函數。執行器接受兩個參數,分別是resolve()函數和reject()函數。執行器成功完成時調用resolve()函數,反之,失敗時則調用reject()函數

複製代碼
// Node.js 範例
let fs = require("fs");
function readFile(filename) {
    return new Promise(function(resolve, reject) {
        // 觸發異步操做
        fs.readFile(filename, { encoding: "utf8" }, function(err, contents) {
            // 檢查錯誤
            if (err) {
                reject(err);
                return;
            }
            // 讀取成功
            resolve(contents);
        });
    });
}
let promise = readFile("example.txt");
// 同時監聽完成與拒絕
promise.then(function(contents) {
    // 完成
    console.log(contents);
}, function(err) {
    // 拒絕
    console.error(err.message);
});
複製代碼

  在這個示例中,用Promise包裹了一個原生Node.js的fs.readFile()異步調用。若是失敗,執行器向reject()函數傳遞錯誤對象;若是成功,執行器向resolve()函數傳遞文件內容

  readFile()方法被調用時執行器會馬上執行,在執行器中,不管是調用resolve()仍是reject(),都會向任務隊列中添加一個任務來解決這個Promise。若是曾經使用過setTimeout()或setInterval()函數,應該熟悉這種名爲任務編排(job schedhling)的過程。當編排任務時,會向任務隊列中添加一個新任務,並明確指定將任務延後執行。例如,使用setTimeout()函數能夠指定將任務添加到隊列前的延時

// 在 500 毫秒以後添加此函數到做業隊列
setTimeout(function() {
    console.log("Timeout");
}, 500);
console.log("Hi!");

  這段代碼編排了一個500 ms後才被添加到任務隊列的任務,兩次console.log()調用分別輸出如下內容

Hi!
Timeout

  因爲有500ms的延時,於是傳入setTimeout()的函數在console.log("Hi!")輸出"Hi"以後才輸出"Timeout"

  Promise具備相似的工做原理,Promise的執行器會當即執行,而後才執行後續流程中的代碼

let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});
console.log("Hi!");

  這段代碼的輸出內容是

promise
Hi !

  調用resolve()後會觸發一個異步操做,傳入then()和catch()方法的函數會被添加到任務隊列中並異步執行

複製代碼
let promise = new Promise(function(resolve, reject) {
    console.log("Promise");
    resolve();
});
promise.then(function() {
    console.log("Resolved.");
});
console.log("Hi!");
複製代碼

  這個示例的輸出內容爲

promise
Hi !
Resolved

  即便在代碼中then()調用位於console.log("Hi!")以前,但其與執行器不一樣,它並無當即執行。這是由於,完成處理程序和拒絕處理程序老是在執行器完成後被添加到任務隊列的末尾

【建立已處理的Promise】

  建立未處理Promise的最好方法是使用Promise的構造函數,這是因爲Promise執行器具備動態性。但若是想用Promise來表示一個已知值,則編排一個只是簡單地給resolve()函數傳值的任務並沒有實際意義,反卻是能夠用如下兩種方法根據特定的值來建立己解決Promise

使用Promise.resolve()

  Promise.resolve()方法只接受一個參數並返回一個完成態的Promise,也就是說不會有任務編排的過程,並且須要向Promise添加一至多個完成處理程序來獲取值

let promise = Promise.resolve(42);
promise.then(function(value) {
    console.log(value); // 42
});

  這段代碼建立了一個已完成Promise,完成處理程序的形參value接受了傳入值42,因爲該Promise永遠不會存在拒絕狀態,於是該Promise的拒絕處理程序永遠不會被調用

使用Promise.reject()

  也能夠經過Promise.reject()方法來建立已拒絕Promise,它與Promise.resolve()很像,惟一的區別是建立出來的是拒絕態的Promise

let promise = Promise.reject(42);
promise.catch(function(value) {
    console.log(value); // 42
});

  任何附加到這個Promise的拒絕處理程序都將被調用,但卻不會調用完成處理程序

  [注意]若是向Promise.resolve()方法或Promise.reject()方法傳入一個Promise,那麼這個Promise會被直接返回

非Promise的Thenable對象

  Promise.resolve()方法和Promise.reject()方法均可以接受非Promise的Thenable對象做爲參數。若是傳入一個非Promise的Thenable對象,則這些方法會建立一個新的Promise,並在then()函數中被調用

  擁有then()方法而且接受resolve和reject這兩個參數的普通對象就是非Promise的Thenable對象

let thenable = {
    then: function(resolve, reject) {
        resolve(42);
    }
};

  在此示例中,Thenable對象和Promise之間只有then()方法這一個類似之處,能夠調用Promise.resolve()方法將Thenable對象轉換成一個已完成Promise

複製代碼
let thenable = {
    then: function(resolve, reject) {
        resolve(42);
    }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
    console.log(value); // 42
});
複製代碼

  在此示例中,Promise.resolve()調用的是thenable.then(),因此Promise的狀態能夠被檢測到。因爲是在then()方法內部調用了resolve(42),所以Thenable對象的Promise狀態是已完成。新建立的已完成狀態Promise p1從Thenable對象接受傳入的值(也就是42),p1的完成處理程序將42賦值給形參value

  可使用與Promise.resolve()相同的過程建立基於Thenable對象的已拒絕Promise

複製代碼
let thenable = {
    then: function(resolve, reject) {
        reject(42);
    }
};
let p1 = Promise.resolve(thenable);
p1.catch(function(value) {
    console.log(value); // 42
});
複製代碼

  此示例與前一個相比,除了Thenable對象是已拒絕狀態外,其他部分比較類似。執行thenable.then()時會用值42建立一個己拒絕狀態的Promise,這個值隨後會被傳入p1的拒絕處理程序

  有了Promise.resolve()方法和Promise.reject()方法,能夠更輕鬆地處理非Promise的Thenable對象。在ES6引入Promise對象以前,許多庫都使用了Thenable對象,因此若是要向後兼容以前已有的庫,則將Thenable對象轉換爲正式Promise的能力就顯得相當重要了。若是不肯定某個對象是否是Promise對象,那麼能夠根據預期的結果將其傳入promise.resolve()方法中或Promise.reject()方法中,若是它是Promise對象,則不會有任何變化

【執行器錯誤】

  若是執行器內部拋出一個錯誤,則Promise的拒絕處理程序就會被調用

複製代碼
let promise = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});
promise.catch(function(error) {
    console.log(error.message); // "Explosion!"
});
複製代碼

  在這段代碼中,執行器故意拋出了一個錯誤,每一個執行器中都隱含一個try-catch塊,因此錯誤會被捕獲並傳入拒絕處理程序。此例等價於

複製代碼
let promise = new Promise(function(resolve, reject) {
    try {
        throw new Error("Explosion!");
    } catch (ex) {
        reject(ex);
    }
});
promise.catch(function(error) {
    console.log(error.message); // "Explosion!"
});
複製代碼

  爲了簡化這種常見的用例,執行器會捕獲全部拋出的錯誤,但只有當拒絕處理程序存在時纔會記錄執行器中拋出的錯誤,不然錯誤會被忽略掉。在早期的時候,開發人員使用Promise會遇到這種問題,後來,JS環境提供了一些捕獲己拒絕Promise的鉤子函數來解決這個問題

 

拒絕處理

  有關Promise的其中一個最具爭議的問題是,若是在沒有拒絕處理程序的狀況下拒絕一個Promise,那麼不會提示失敗信息,這是JS語言中惟一一處沒有強制報錯的地方,一些人認爲這是標準中最大的缺陷

  Promise的特性決定了很難檢測一個Promise是否被處理過

複製代碼
let rejected = Promise.reject(42);
    // 在此刻 rejected 不會被處理
    // 一段時間後……
rejected.catch(function(value) {
    // 如今 rejected 已經被處理了
    console.log(value);
});
複製代碼

  任什麼時候候均可以調用then()方法或catch()方法,不管Promise是否已解決,這兩個方法均可以正常運行,但這樣就很難知道一個Promise什麼時候被處理。在此示例中,Promise被當即拒絕,可是稍後才被處理

  儘管這個問題在將來版本的ES中可能會被解決,可是Node和和瀏覽器環境都已分別作出了一些改變來解決開發者的這個痛點,這些改變不是ES6標準的一部分,不過使用Promise時它們確實是很是有價值的工具

【Node.js環境的拒絕處理】

  在Node.js中,處理Promise拒絕時會觸發process對象上的兩個事件

  一、unhandledRejection

  在一個事件循環中,當Promise被拒絕,而且沒有提供拒絕處理程序時被調用

  二、rejectionHandled

  在一個事件循環後,當Promise被拒絕,而且沒有提供拒絕處理程序時被調用

  設計這些事件是用來識別那些被拒絕卻又沒被處理過的Promise的

  拒絕緣由(一般是一個錯誤對象)及被拒絕的Promise做爲參數被傳入unhandledRejection事件處理程序中

複製代碼
let rejected;
process.on("unhandledRejection", function(reason, promise) {
    console.log(reason.message); // "Explosion!"
    console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
複製代碼

  這個示例建立了一個已拒絕Promise和一個錯誤對象,並監聽了unhandledRejection事件,事件處理程序分別接受錯誤對象和Promise做爲它的兩個參數

  rejectionHandled事件處理程序只有一個參數,也就是被拒絕的Promise

複製代碼
let rejected;
process.on("rejectionHandled", function(promise) {
    console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
// 延遲添加拒絕處理函數
setTimeout(function() {
    rejected.catch(function(value) {
        console.log(value.message); // "Explosion!"
    });
}, 1000);
複製代碼

  這裏的rejectionHandled事件在拒絕處理程序最後被調用時觸發,若是在建立rejected以後直接添加拒絕處理程序,那麼rejectionHandled事件不會被觸發,由於rejected建立的過程與拒絕處理程序的調用在同一個事件循環中,此時rejectionHandled事件還沒有生效

  經過事件rejectionHandled和事件unhandledRejection將潛在未處理的拒絕存儲爲一個列表,等待一段時間後檢查列表便可以正確地跟蹤潛在的未處理拒絕。例以下面這個簡單的未處理拒絕跟蹤器

複製代碼
let possiblyUnhandledRejections = new Map();
// 當一個拒絕未被處理,將其添加到 map
process.on("unhandledRejection", function(reason, promise) {
    possiblyUnhandledRejections.set(promise, reason);
});
process.on("rejectionHandled", function(promise) {
    possiblyUnhandledRejections.delete(promise);
});
setInterval(function() {
    possiblyUnhandledRejections.forEach(function(reason, promise) {
        console.log(reason.message ? reason.message : reason);
        // 作點事來處理這些拒絕
        handleRejection(promise, reason);
    });
    possiblyUnhandledRejections.clear();
}, 60000);
複製代碼

  這段代碼使用Map集合來存儲Promise及其拒絕緣由,每一個Promise鍵都有一個拒絕緣由的相關值。每當觸發unhandledRejection事件時,會向Map集合中添加一組Promise及拒絕緣由;每當觸發rejectionHandled事件時,已處理的Promise會從Map集合中移除。結果是,possiblyUnhandledRejections會隨着事件調用不斷擴充或收縮。setInterval()調用會按期檢查列表,將可能未處理的拒絕輸出到控制檯(實際上會經過其餘方式記錄或者直接處理掉這個拒絕)。在這個示例中使用的是Map集合而不是WeakMap集合,這是由於須要按期檢查Map集合來確認一個Promise是否存在,而這是WeakMap沒法實現的

  儘管這個示例針對Node.js設計,可是瀏覽器也實現了一套相似的機制來提示開發者哪些拒絕尚未被處理

【瀏覽器環境的拒絕處理】

  瀏覽器也是經過觸發兩個事件來識別未處理的拒絕的,雖然這些事件是在window對象上觸發的,但實際上與Node.js中的徹底等效

  一、unhandledrejection

  在一個事件循環中,當promise被拒絕,而且沒有提供拒絕處理程序時被調用

  二、rejectionhandled

  在一個事件循環後,當promise被拒絕,而且沒有提供拒絕處理程序時被調用

  在Node.js實現中,事件處理程序接受多個獨立參數:而在瀏覽器中,事件處理程序接受一個有如下屬性的事件對象做爲參數

type  事件名稱 ("unhandledrejection"或"rejectionhandled")
promise 被拒絕的promise對象
reason 來自promise的拒絕值

  瀏覽器實現中的另外一處不一樣是,在兩個事件中均可以使用拒絕值(reason)

複製代碼
let rejected;
window.onunhandledrejection = function(event) {
    console.log(event.type); // "unhandledrejection"
    console.log(event.reason.message); // "Explosion!"
    console.log(rejected === event.promise); // true
};
window.onrejectionhandled = function(event) {
    console.log(event.type); // "rejectionhandled"
    console.log(event.reason.message); // "Explosion!"
    console.log(rejected === event.promise); // true
};
rejected = Promise.reject(new Error("Explosion!"));
複製代碼

  這段代碼用DOM0級記法的onunhandledrejection和onrejectionhandled給兩個事件處理程序賦值,固然也可使用addEventListener("unhandledrejection") 和addEventListener("rejectionhandled"),每一個事件處理程序接受一個含有被拒絕Promise信息的事件對象,該對象的屬性type、promise和reason在這兩個事件處理程序中都可使用

  在瀏覽器中,跟蹤未處理拒絕的代碼也與Node.js中的很是類似

複製代碼
let possiblyUnhandledRejections = new Map();
// 當一個拒絕未被處理,將其添加到 map
window.onunhandledrejection = function(event) {
    possiblyUnhandledRejections.set(event.promise, event.reason);
};
window.onrejectionhandled = function(event) {
    possiblyUnhandledRejections.delete(event.promise);
};
setInterval(function() {
    possiblyUnhandledRejections.forEach(function(reason, promise) {
        console.log(reason.message ? reason.message : reason);
        // 作點事來處理這些拒絕
        handleRejection(promise, reason);
    });
    possiblyUnhandledRejections.clear();
}, 60000);
複製代碼

  瀏覽器中的實現與Node.js中的幾乎徹底相同,兩者都是用一樣的方法將promise及其拒絕值存儲在Map集合中,而後再進行檢索。惟一的區別是,在事件處理程序中檢索信息的位置不一樣

 

串聯

  至此,看起來好像Promise只是將回調函數和setTimeout()函數結合起來,並在此基礎上作了一些改進。但Promise所能實現的遠超咱們目之所及,尤爲是不少將Promise串聯起來實現更復雜的異步特性的方法

  每次調用then()方法或catch()方法時實際上建立並返回了另外一個Promise,只有當第一個Promise完成或被拒絕後,第二個纔會被解決

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
p1.then(function(value) {
    console.log(value);
}).then(function() {
    console.log("Finished");
});
複製代碼

  這段代碼輸出如下內容

42
Finished

  調用p1.then()後返回第二個Promise,緊接着又調用了它的then()方法,只有當第一個Promise被解決以後纔會調用第二個then()方法的完成處理程序。若是將這個示例拆解開,看起來是這樣的

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = p1.then(function(value) {
    console.log(value);
})
p2.then(function() {
    console.log("Finished");
});
複製代碼

  在這個非串聯版本的代碼中,調用p1.then()的結果被存儲在了p2中,而後p2.then()被調用來添加最終的完成處理程序

【捕獲錯誤】

  在以前的示例中,完成處理程序或拒絕處理程序中可能發生錯誤,而Promise鏈能夠用來捕獲這些錯誤

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
p1.then(function(value) {
    throw new Error("Boom!");
}).catch(function(error) {
    console.log(error.message); // "Boom!"
});
複製代碼

  在這段代碼中,p1的完成處理程序拋出了一個錯誤,鏈式調用第二個Promise的catch()方法後,能夠經過它的拒絕處理程序接收這個錯誤。若是拒絕處理程序拋出錯誤,也能夠經過相同的方式接收到這個錯誤

複製代碼
let p1 = new Promise(function(resolve, reject) {
    throw new Error("Explosion!");
});
p1.catch(function(error) {
    console.log(error.message); // "Explosion!"
    throw new Error("Boom!");
}).catch(function(error) {
    console.log(error.message); // "Boom!"
});
複製代碼

  此處的執行器拋出錯誤並觸發Promise p1的拒絕處理程序,這個處理程序又拋出另一個錯誤,而且被第二個Promise的拒絕處理程序捕獲。鏈式Promise調用能夠感知到鏈中其餘Promise的錯誤

  [注意]務必在Promise鏈的末尾留有一個拒絕處理程序以確保可以正確處理全部可能發生的錯誤

【Promise鏈的返回值】

  Promise鏈的另外一個重要特性是能夠給下游Promise傳遞數據,已經知道了從執行器resolve()處理程序到Promise完成處理程序的數據傳遞過程,若是在完成處理程序中指定一個返回值,則能夠沿着這條鏈繼續傳遞數據

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
p1.then(function(value) {
    console.log(value); // "42"
    return value + 1;
}).then(function(value) {
    console.log(value); // "43"
});
複製代碼

  執行器傳入的value爲42,p1的完成處理程序執行後返回value+1也就是43。這個值隨後被傳給第二個Promise的完成處理程序並輸出到控制檯

  在拒絕處理程序中也能夠作相同的事情,當它被調用時能夠返回一個值,而後用這個值完成鏈條中後續的Promise

複製代碼
let p1 = new Promise(function(resolve, reject) {
    reject(42);
});
p1.catch(function(value) {
    // 第一個完成處理函數
    console.log(value); // "42"
    return value + 1;
}).then(function(value) {
    // 第二個完成處理函數
    console.log(value); // "43"
});
複製代碼

  在這個示例中,執行器調用reject()方法向Promise的拒絕處理程序傳入值42,最終返回value+1。拒絕處理程序中返回的值仍可用在下一個Promise的完成處理程序中,在必要時,即便其中一個Promise失敗也能恢復整條鏈的執行

【在Promise鏈中返回Promise】

  在Promise間能夠經過完成和拒絕處理程序中返回的原始值來傳遞數據,但若是返回的是Promise對象,會經過一個額外的步驟來肯定下一步怎麼走

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});
p1.then(function(value) {
    // 第一個完成處理函數
    console.log(value); // 42
    return p2;
}).then(function(value) {
    // 第二個完成處理函數
    console.log(value); // 43
});
複製代碼

  在這段代碼中,p1編排的任務解決並傳入42,而後p1的完成處理程序返回一個已解決狀態的Promise p2,因爲p2已經被完成,所以第二個完成處理程序被調用。若是p2被拒絕,則調用拒絕處理程序

  關於這個模式,最須要注意的是,第二個完成處理程序被添加到了第三個Promise而不是p2

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});
let p3 = p1.then(function(value) {
    // 第一個完成處理函數
    console.log(value); // 42
    return p2;
});
p3.then(function(value) {
    // 第二個完成處理函數
    console.log(value); // 43
});
複製代碼

  很明顯的是,此處第二個完成處理程序被添加到p3而非p2,這個差別雖小但很是重要,若是p2被拒絕那麼第二個完成處理程序將不會被調用

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
    reject(43);
});
p1.then(function(value) {
    // 第一個完成處理函數
    console.log(value); // 42
    return p2;
}).then(function(value) {
    // 第二個完成處理函數
    console.log(value); // 永不被調用
});
複製代碼

  在這個示例中,因爲p2被拒絕了,所以完成處理程序永遠不會被調用。無論怎樣,仍是能夠添加一個拒絕處理程序

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
    reject(43);
});
p1.then(function(value) {
    // 第一個完成處理函數
    console.log(value); // 42
    return p2;
}).catch(function(value) {
    // 拒絕處理函數
    console.log(value); // 43
});
複製代碼

  p2被拒絕後,拒絕處理程序被調用並傳入p2的拒絕值43

  在完成或拒絕處理程序中返回Thenable對象不會改變Promise執行器的執行時機,先定義的Promise的執行器先執行,後定義的後執行,以此類推。返回Thenable對象僅容許爲這些promise結果定義額外的響應。在完成處理程序中建立新的Promise能夠推遲完成處理程序的執行

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
p1.then(function(value) {
    console.log(value); // 42
    // 建立一個新的 promise
    let p2 = new Promise(function(resolve, reject) {
        resolve(43);
    });
    return p2
}).then(function(value) {
    console.log(value); // 43
});
複製代碼

  在此示例中,在p1的完成處理程序裏建立了一個新的Promise,直到p2被完成纔會執行第二個完成處理程序。若是想在一個Promise被解決後觸發另個promise,那麼這個模式會頗有幫助

 

響應多個

  若是想經過監聽多個Promise來決定下一步的操做,可使用ES6提供的Promise.all()和Promise.race()這兩個方法

【Promise.all()】

  Promise.all()方法只接受一個參數並返回一個Promise,該參數是一個含有多個受監視Promise的可迭代對象(如一個數組),只有當可迭代對象中全部Promise都被解決後返回的Promise纔會被解決,只有當可迭代對象中全部Promise都被完成後返回的Promise纔會被完成

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
    resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
    console.log(Array.isArray(value)); // true
    console.log(value[0]); // 42
    console.log(value[1]); // 43
    console.log(value[2]); // 44
});
複製代碼

  在這段代碼中,每一個Promise解決時都傳入一個數字,調用Promise.all()方法建立Promise p4,最終當Promise p一、p2和p3都處於完成狀態後p4才被完成。傳入p4完成處理程序的結果是一個包含每一個解決值(42.43和44)的數組,這些值按照Promise被解決的順序存儲,因此能夠根據每一個結果來匹配對應的Promise

  全部傳入Promise.all()方法的Promise只要有一個被拒絕,那麼返回的Promise沒等全部Promise都完成就當即被拒絕

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
    reject(43);
});
let p3 = new Promise(function(resolve, reject) {
    resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function(value) {
    console.log(Array.isArray(value)) // false
    console.log(value); // 43
});
複製代碼

  在這個示例中,p2被拒絕並傳入值43,沒等p1或p3結束執行,p4的拒絕處理程序就當即被調用。(p1和p3的執行過程會結束,只是p4並未等待)

  拒絕處理程序老是接受一個值而非數組,該值來自被拒絕Promise的拒絕值。在本示例中,傳入拒絕處理程序的43表示該拒絕來自p2

【Promise.race()】

  Promise.race()方法監聽多個Promise的方法稍有不一樣:它也接受含多個受監視Promise的可迭代對象做爲惟一參數並返回一個Promise,但只要有一個Promise被解決返回的Promise就被解決,無須等到全部Promise都被完成。一旦數組中的某個Promise被完成,Promise.race()方法也會像Promise.all()方法同樣返回一個特定的Promise

複製代碼
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
    resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
    console.log(value); // 42
});
複製代碼

  在這段代碼中,p1建立時便處於已完成狀態,其餘Promise用於編排任務。隨後,p4的完成處理程序被調用並傳入值42,其餘Promise則被忽略。實際上,傳給Promise.race()方法的Promise會進行競選,以決出哪個先被解決,若是先解決的是已完成Promise,則返回己完成Promise;若是先解決的是已拒絕Promise,則返回已拒絕Promise

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = Promise.reject(43);
let p3 = new Promise(function(resolve, reject) {
    resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.catch(function(value) {
    console.log(value); // 43
});
複製代碼

  此時,因爲p2己處於被拒絕狀態,於是當Promise.race()方法被調用時p4也被拒絕了,儘管p1和p3最終被完成,但因爲是發生在p2被拒後,所以它們的結果被忽略掉

 

繼承

  Promise與其餘內建類型同樣,也能夠做爲基類派生其餘類,因此能夠定義本身的Promise變量來擴展內建Promise的功能。例如,假設建立一個既支持then()方法和catch()方法又支持success()方法和failure()方法的Promise,則能夠這樣建立該Promise類型

複製代碼
class MyPromise extends Promise {
    // 使用默認構造器
    success(resolve, reject) {
        return this.then(resolve, reject);
}
    failure(reject) {
        return this.catch(reject);
    }
}
let promise = new MyPromise(function(resolve, reject) {
    resolve(42);
});
promise.success(function(value) {
    console.log(value); // 42
}).failure(function(value) {
    console.log(value);
});
複製代碼

  在這個示例中,派生自Promise的MyPromise擴展了另外兩個方法模仿resolve()的success()方法以及模仿reject()的failure()方法

  這兩個新增方法都經過this來調用它模仿的方法,派生Promise與內建Promise的功能同樣,只不過多了success()和failure()這兩個能夠調用的方法

  因爲靜態方法會被繼承,所以派生的Promise也擁有MyPromise.resolve()、MyPromise.reject()、MyPromise.race()和MyPromise. all() 這 4 個方法,後兩者與內建方法徹底一致,而前兩者卻稍有不一樣

  因爲MyPromise.resolve()方法和MyPromise.reject()方法經過Symbol.species屬性來決定返回Promise的類型,故調用這兩個方法時不管傳入什麼值都會返回一個MyPromise的實例。若是將內建Promise做爲參數傳入其餘方法,則這個Promise將被解決或拒絕,而後該方法將會返回一個新的MyPromise,因而就能夠給它的成功處理程序及失敗處理程序賦值

複製代碼
let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});
let p2 = MyPromise.resolve(p1);
p2.success(function(value) {
    console.log(value); // 42
});
console.log(p2 instanceof MyPromise); // true
複製代碼

  這裏的p1是一個內建Promise,被傳入MyPromise.resolve()方法後獲得結果p2,它是MyPromise的一個實例,來自p1的解決值被傳入完成處理程序

  傳入MyPromise.resolve()方法或MyPromise.reject()方法的MyPromise實例未經解決便直接返回。在其餘方面,這兩個方法的行爲與Promise.resolve()和Promise.reject()很像

 

異步

  以前,介紹過生成器並展現瞭如何在異步任務執行中使用它

複製代碼
let fs = require("fs");
function run(taskDef) {
    // 建立迭代器,讓它在別處可用
    let task = taskDef();
    // 開始任務
    let result = task.next();
    // 遞歸使用函數來保持對 next() 的調用
    function step() {
        // 若是還有更多要作的
        if (!result.done) {
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }
                    result = task.next(data);
                    step();
                });
            } else {
                result = task.next(result.value);
                step();
            }
        }
    }
    // 開始處理過程
    step();
}
// 定義一個函數來配合任務運行器使用
function readFile(filename) {
    return function(callback) {
        fs.readFile(filename, callback);
    };
}
// 運行一個任務
run(function*() {
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});
複製代碼

  這個實現會致使一些問題。首先,在返回值是函數的函數中包裹每個函數會使人感到困惑,這句話自己也是如此;其次,沒法區分用做任務執行器回調函數的返回值和一個不是回調函數的返回值

  只要每一個異步操做都返回Promise,就能夠極大地簡化並通用化這個過程。以Promise做爲通用接口用於全部異步代碼能夠簡化任務執行器

複製代碼
let fs = require("fs");
function run(taskDef) {
    // 建立迭代器
    let task = taskDef();
    // 啓動任務
    let result = task.next();
    // 遞歸使用函數來進行迭代
    (function step() {
        // 若是還有更多要作的
        if (!result.done) {
            // 決議一個 Promise ,讓任務處理變簡單
            let promise = Promise.resolve(result.value);
            promise.then(function(value) {
                result = task.next(value);
                step();
            }).catch(function(error) {
                result = task.throw(error);
                step();
            });
        }
    }());
}
// 定義一個函數來配合任務運行器使用
function readFile(filename) {
    return new Promise(function(resolve, reject) {
        fs.readFile(filename, function(err, contents) {
            if (err) {
                reject(err);
            } else {
                resolve(contents);
            }    
        });
    });
}
// 運行一個任務
run(function*() {
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});
複製代碼

  在這個版本的代碼中,一個通用的run()函數執行生成器建立了一個迭代器,它調用task.next()方法來啓動任務並遞歸調用step()方法直到迭代器完成

  在step()函數中,若是有更多任務,那麼result.done的值爲false,此時的result.value應該是一個Promise,調用Promise.resolve()是爲了防止函數不返回Promise。(傳入Promise.resolve()的Promise直接經過,傳入的非Promise會被包裹成一個Promise)接下來,添加完成處理程序提取Promise的值並將其傳回迭代器。而後在step()函數調用自身以前結果會被賦值給下一個生成的結果

  拒絕處理程序將全部拒絕結果存儲到一個錯誤對象中,而後經過task.throw()方法將錯誤對象傳回迭代器,若是在任務中捕獲到錯誤,結果會被賦值給下一個生成結果。最後繼續在catch()內部調用step()函數

  這個run()函數能夠運行全部使用yield實現異步代碼的生成器,並且不會將Promise或回調函數暴露給開發者。事實上,因爲函數調用的返回值總會被轉換成一個Promise,所以能夠返回一些非Promise的值,也就是說,用yield調用同步或異步方法均可以正常運行,永遠不須要檢查返回值是否爲Promise

  惟一須要關注的是像readFile()這樣的異步函數,其返回的是一個能被正確識別狀態的Promise,因此調用Node.js的內建方法時不能使用回調函數,須將其轉換爲返回Promise的函數

【將來的異步任務執行】

  JS正在引入一種用於執行異步任務的更簡單的語法,例如,await語法致力於替代基於Promise的示例。其基本思想是用async標記的函數代替生成器,用await代替yield來調用函數

(async function() {
    let contents = await readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});

  在函數前添加關鍵字async表示該函數以異步模式運行,await關鍵字表示調用readFile("config.json")的函數應該返回一個Promise,不然,響應應該被包裹在Promise中。若是Promise被拒絕則await應該拋出錯誤,不然經過Promise來返回值。最後的結果是,能夠按照同步方式編寫異步代碼,惟一的開銷是一個基於迭代器的狀態機

相關文章
相關標籤/搜索