前面的話
JS
有很多強大的功能,其中一個是它可以輕松地搞定異步編程。作為一門為Web
而生的語言,它從一開始就需要能夠響應異步的用戶交互,如點擊和按鍵操作等。Node.js
用回調函數代替了事件,使異步編程在JS
領域更加流行。但當更多程序開始使用異步編程時,事件和回調函數卻不能滿足開發者想要做的所有事情,它們還不夠強大,而Promise
就是這些問題的解決方案
Promise
可以實現其他語言中類似Future
和Deferred
一樣的功能,是另一種異步編程的選擇,它既可以像事件和回調函數一樣指定稍后執行的代碼,也可以明確指示代碼是否成功執行。基于這些成功或失敗的狀態,為了讓代碼更容易理解和調試,可以鏈式地編寫Promise
。本文將詳細介紹Promise
和異步編程
引入
JS
引擎是基于單線程(Single-threaded)
事件循環的概念構建的,同一時刻只允許一個代碼塊在執行,與之相反的是像Java
和C++
一樣的語言,它們允許多個不同的代碼塊同時執行。對于基于線程的軟件而言,當多個代碼塊同時訪問并改變狀態時,程序很難維護并保證狀態不會出錯
JS
引擎同一時刻只能執行一個代碼塊,所以需要跟蹤即將運行的代碼,那些代碼被放在一個任務隊列(job queue)
中,每當一段代碼準備執行時,都會被添加到任務隊列。每當JS
引擎中的一段代碼結束執行,事件循環(event toop)
會執行隊列中的下一個任務,它是JS
引擎中的一段程序,負責監控代碼執行并管理任務隊列。隊列中的任務會從第一個一直執行到最后一個
【事件模型】
用戶點擊按鈕或按下鍵盤上的按鍵會觸發類似
onclick
這樣的事件,它會向任務隊列添加一個新任務來響應用戶的操作,這是JS
中最基礎的異步編程形式,直到事件觸發時才執行事件處理程序,且執行時上下文與定義時的相同
let button = document.getElementById("my-btn");
button.onclick = function(event) {
console.log("Clicked");
};
在這段代碼中,單擊
button
后會執行console.log("clicked")
,賦值給onclick
的函數被添加到任務隊列中,只有當前面的任務都完成后它才會被執行事件模型適用于處理簡單的交互,然而將多個獨立的異步調用連接在一起會使程序更加復雜,因為必須跟蹤每個事件的事件目標(如此示例中的
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
可能會進入到以下兩個狀態中的其中一個
1、Fulfilled
Promise
異步操作成功完成
2、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
處于己處理狀態,在這之后添加到任務隊列中的處理程序仍將執行。所以無論何時都可以添加新的完成處理程序或拒絕處理程序,同時也可以保證這些處理程序能被調用
[圖片上傳中...(image-2a3d4c-1550134626393-63)]
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
對象上的兩個事件
1、unhandledRejection
在一個事件循環中,當Promise
被拒絕,并且沒有提供拒絕處理程序時被調用
2、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();
// 當一個拒絕未被處理,將其添加到
mapprocess.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
中的完全等效
1、unhandledrejection
在一個事件循環中,當promise
被拒絕,并且沒有提供拒絕處理程序時被調用
2、rejectionhandled
在一個事件循環后,當promise
被拒絕,并且沒有提供拒絕處理程序時被調用
在
Node.js
實現中,事件處理程序接受多個獨立參數:而在瀏覽器中,事件處理程序接受一個有以下屬性的事件對象作為參數
type 事件名稱 ("unhandledrejection"或"rejectionhandled"), promise 被拒絕的promise對象, reason 來自promise的拒絕值
- 瀏覽器實現中的另一處不同是,在兩個事件中都可以使用拒絕值
(reason)
[圖片上傳中...(image-2ae8e3-1550134626392-41)]
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!"));
這段代碼用
DOM
0級記法的onunhandledrejection
和onrejectionhandled
給兩個事件處理程序賦值,當然也可以使用addEventListener("unhandledrejection")
和addEventListener("rejectionhandled")
,每個事件處理程序接受一個含有被拒絕Promise
信息的事件對象,該對象的屬性type
、promise
和reason
在這兩個事件處理程序中均可使用在瀏覽器中,跟蹤未處理拒絕的代碼也與
Node.js
中的非常相似
let possiblyUnhandledRejections = new Map();// 當一個拒絕未被處理,將其添加到
mapwindow.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");
});
這段代碼輸出以下內容
42Finished
- 調用
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 p1
、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
來返回值。最后的結果是,可以按照同步方式編寫異步代碼,唯一的開銷是一個基于迭代器的狀態機q