[1]引入 [2]基礎 [3]拒絕處理 [4]串聯 [5]響應多個 [6]繼承 [7]非同步 ...
前面的話
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處於己處理狀態,在這之後添加到任務隊列中的處理程式仍將執行。所以無論何時都可以添加新的完成處理程式或拒絕處理程式,同時也可以保證這些處理程式能被調用
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(); // 當一個拒絕未被處理,將其添加到 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中的完全等效
1、unhandledrejection
在一個事件迴圈中,當promise被拒絕,並且沒有提供拒絕處理程式時被調用
2、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 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) { // 創