術(shù)語:決議、完成以及拒絕
為什么單詞resolve(比如在Promise.resolve(..)中)如果用于表達結(jié)果可能是完成也可能是拒絕的話,既沒有歧義,并且也確實更精確:
var rejectedTh = {
then: function (resolved, rejected) {
rejected("Oops");
}
};
var rejectedPr = Promise.resolve(rejectedTh);
前面已經(jīng)介紹過,Promise.resolve(..)會將傳入的真正Promise直接返回,對傳入的thenable則會展開。如果這個thenable展開得到一個拒絕狀態(tài),那么從Promise.resolve(..)返回的Promise實際上就是這同一個拒絕狀態(tài)。
Promise(..)構(gòu)造器的第一個參數(shù)回調(diào)會展開thenable(和Promise.resolve(..)一樣)或真正的Promise:
var rejectedPr = new Promise(function (resolve, reject) {
// 用一個被拒絕的promise完成這個promise
resolve(Promise.reject("Oops"));
});
rejectedPr.then(
function fulfilled() {
// 永遠不會到達這里
},
function rejected(err) {
console.log(err); // "Oops"
}
);
現(xiàn)在應(yīng)該很清楚了,Promise(..)構(gòu)造器的第一個回調(diào)參數(shù)的恰當(dāng)稱謂是resolve(..)。
前面提到的reject(..)不會像resolve(..)一樣進行展開。如果向reject(..)傳入一個Promise/thenable值,它會把這個值原封不動地設(shè)置為拒絕理由。后續(xù)的拒絕處理函數(shù)接受到的是你實際傳給reject(..)的那個Promise/thenable,而不是其低層的立即值。
不過,現(xiàn)在我們再來關(guān)注一下提供給then(..)的回調(diào)。它們(在文獻和代碼中)應(yīng)該怎么命名呢?我的建議是fullfilled(..)和reject(..)。
function fulfilled(msg) {
console.log(msg);
}
function rejected(err) {
console.error(err);
}
p.then(
fulfilled,
rejected
);
對then(..)的第一個參數(shù)來說,毫無疑義,總是處理完成的情況,所以不需要使用標識兩種狀態(tài)的術(shù)語“resolve”。這里提一下,ES6規(guī)范將這兩個回調(diào)命名為onFulfilled(..)和onRjected(..),所以這兩個術(shù)語很準確。
錯誤處理
對多數(shù)開發(fā)者來說,錯誤處理最自然的形式就是同步的try..catch構(gòu)造。遺憾的是,它只能是同步的,無法用于異步代碼模式:
function foo() {
setTimeout(function () {
baz.bar();
}, 100);
}
try {
foo();
// 后面從 `baz.bar()` 拋出全局錯誤
}
catch (err) {
// 永遠不會到達這里
}
try..catch當(dāng)然很好,但是無法跨異步操作工作。也就是說,還需要一些額外的環(huán)境支持。
在回調(diào)中,一些模式化的錯誤處理方式已經(jīng)出現(xiàn),最值得一提的是error-first回調(diào)風(fēng)格:
function foo(cb) {
setTimeout(function () {
try {
cb(null, 1); // 成功!
}
catch (err) {
cb(err);
}
}, 100);
}
foo(function (err, val) {
if (err) {
console.error(err); // 煩 :(
}
else {
console.log(val);//1
}
});
只有在baz.bar()調(diào)用會同步地立即成功或失敗的情況下,這里的try..catch才能工作。如果baz.bar()本身有自己的異步完成函數(shù),其中的任何異步錯誤都將無法捕捉到。
傳給foo(..)的回調(diào)函數(shù)保留第一個參數(shù)err,用于在出錯時接受到信號。如果其存在的話,就認為出錯,否則就認為是成功。
嚴格來說,這一類錯誤處理是支持異步的,但完全無法很好地組合。多級error-first回調(diào)交織在一起,再加上這些無所不在的if檢查語句,都不可避免地導(dǎo)致了回調(diào)地獄的風(fēng)險。
var p = Promise.resolve(42);
p.then(
function fulfilled(msg) {
// 數(shù)字沒有string函數(shù),所以會拋出錯誤
console.log(msg.toLowerCase());
},
function rejected(err) {
// 永遠不會到達這里
}
);
如果msg.toLowerCase()合法地拋出一個錯誤(事實確實如此?。瑸槭裁次覀兊腻e誤處理函數(shù)沒有得到通知呢?正如前面解釋過,這是因為那個錯誤處理函數(shù)是為promise p準備的,而這個promise已經(jīng)用值42填充了。promise p是不可變的,所以唯一可以被通知這個錯誤的promise是從p.then(..)返回的那一個,但我們在此例中沒有捕捉。
這應(yīng)該清晰地解釋了為什么Promise的錯誤處理易于出錯。這非常容易造成錯誤被吞掉,而這極少是出于你的本意。
絕望的陷阱
毫無疑問,Promise錯誤處理就是一個“絕望的陷阱”設(shè)計。默認情況下,它假定你想要Promise狀態(tài)吞掉所有的錯誤。如果你忘了查看這個狀態(tài),這個錯誤就會默默地(通常是絕望地)在暗處凋零死掉。
為了避免丟失被忽略和拋棄的Promise錯誤,一些開發(fā)者表示,Promise鏈的一個最佳實踐就是最后總以一個catch(..)結(jié)束:
var p = Promise.resolve(42);
p.then(
function fulfilled(msg) {
// 數(shù)字沒有string函數(shù),所以會拋出錯誤
console.log(msg.toLowerCase());
}
)
.catch(handleErrors);
因為我們沒有為then(..)傳入拒絕處理函數(shù),所以默認的處理函數(shù)被替換掉了,而這僅僅是把錯誤傳遞給了鏈中的下一個promise。因此,進入p的錯誤以及p之后進入其決議(就像msg.toLowerCase())的錯誤都會傳遞到最后的handleErrors(..)。
如果handleErrors(..)本身內(nèi)部也有錯誤怎么辦呢?誰來捕捉它?還有一個沒人處理的promise:catch(..)返回的那一個。我們沒有捕獲這個promise的結(jié)果,也沒有為其注冊拒絕處理函數(shù)。
你并不能簡單地在這個鏈尾端添加一個新的catch(..),因為它很可能會失敗。任何Promise鏈的最后一步,不管是什么,總是存在著在未被查看的Promise中出現(xiàn)未捕獲錯誤的可能性,盡管這種可能性越來越低。
處理未捕獲的情況
Promsie 應(yīng)該添加一個done(..) 函數(shù),從本質(zhì)上標識Promsie 鏈的結(jié)束。done(..) 不會創(chuàng)建和返回Promise,所以傳遞給done(..) 的回調(diào)顯然不會報告一個并不存在的鏈接Promise 的問題。
那么會發(fā)生什么呢?它的處理方式類似于你可能對未捕獲錯誤通常期望的處理方式:done(..) 拒絕處理函數(shù)內(nèi)部的任何異常都會被作為一個全局未處理錯誤拋出(基本上是在開發(fā)者終端上)。代碼如下:
var p = Promise.resolve(42);
p.then(function fulfilled(msg) {
// 數(shù)字沒有string函數(shù),所以會拋出錯誤
console.log(msg.toLowerCase());
}).done(null, handleErrors);
// 如果handleErrors(..)引發(fā)了自身的異常,會被全局拋出到這里
相比沒有結(jié)束的鏈接或者任意時長的定時器,這種方案看起來似乎更有吸引力。但最大的問題是,它并不是ES6 標準的一部分,所以不管聽起來怎么好,要成為可靠的普遍解決方案,它還有很長一段路要走。
瀏覽器有一個特有的功能是我們的代碼所沒有的:它們可以跟蹤并了解所有對象被丟棄以及被垃圾回收的時機。所以,瀏覽器可以追蹤Promise 對象。如果在它被垃圾回收的時候其中有拒絕,瀏覽器就能夠確保這是一個真正的未捕獲錯誤,進而可以確定應(yīng)該將其報告到開發(fā)者終端。
Promise 模式
Promise.all([..])
在異步序列中(Promise 鏈),任意時刻都只能有一個異步任務(wù)正在執(zhí)行——步驟2 只能在步驟1 之后,步驟3 只能在步驟2 之后。但是,如果想要同時執(zhí)行兩個或更多步驟(也就是“并行執(zhí)行”),要怎么實現(xiàn)呢?
在經(jīng)典的編程術(shù)語中,門(gate)是這樣一種機制要等待兩個或更多并行/ 并發(fā)的任務(wù)都完成才能繼續(xù)。它們的完成順序并不重要,但是必須都要完成,門才能打開并讓流程控制繼續(xù)。
在Promise API 中,這種模式被稱為all([ .. ])。
// request(..)是一個Promise-aware Ajax工具
// 就像我們在本章前面定義的一樣
var p1 = request("http://some.url.1/");
var p2 = request("http://some.url.2/");
Promise.all([p1, p2]).then(function(msgs) {
// 這里,p1和p2完成并把它們的消息傳入
return request("http://some.url.3/?v=" + msgs.join(","));
}).then(function(msg) {
console.log(msg);
});
從Promise.all([ .. ]) 返回的主promise 在且僅在所有的成員promise 都完成后才會完成。如果這些promise 中有任何一個被拒絕的話,主Promise.all([ .. ])promise 就會立即被拒絕,并丟棄來自其他所有promise 的全部結(jié)果。
Promise.race([..])
盡管Promise.all([ .. ]) 協(xié)調(diào)多個并發(fā)Promise 的運行,并假定所有Promise 都需要完成,但有時候你會想只響應(yīng)“第一個跨過終點線的Promise”,而拋棄其他Promise。
這種模式傳統(tǒng)上稱為門閂,但在Promise 中稱為競態(tài)。
Promise.race([ .. ]) 也接受單個數(shù)組參數(shù)。這個數(shù)組由一個或多個Promise、thenable 或立即值組成。立即值之間的競爭在實踐中沒有太大意義,因為顯然列表中的第一個會獲勝,就像賽跑中有一個選手是從終點開始比賽一樣!
與Promise.all([ .. ]) 類似,一旦有任何一個Promise 決議為完成,Promise.race([ .. ])就會完成;一旦有任何一個Promise 決議為拒絕,它就會拒絕。
// request(..)是一個支持Promise的Ajax工具
// 就像我們在本章前面定義的一樣
var p1 = request("http://some.url.1/");
var p2 = request("http://some.url.2/");
Promise.race([p1, p2]).then(function(msg) {
// p1或者p2將贏得這場競賽
return request("http://some.url.3/?v=" + msg);
}).then(function(msg) {
console.log(msg);
});
因為只有一個promise 能夠取勝,所以完成值是單個消息,而不是像對Promise.all([ .. ])那樣的是一個數(shù)組。
- 超時競賽
我們之前看到過這個例子,其展示了如何使用Promise.race([ .. ]) 表達Promise 超時模式:
// foo()是一個支持Promise的函數(shù)
// 前面定義的timeoutPromise(..)返回一個promise,
// 這個promise會在指定延時之后拒絕
// 為foo()設(shè)定超時
Promise.race([
foo(), // 啟動foo()
timeoutPromise(3000) // 給它3秒鐘
]).then(function() {
// foo(..)按時完成!
}, function(err) {
// 要么foo()被拒絕,要么只是沒能夠按時完成,
// 因此要查看err了解具體原因
});
在多數(shù)情況下,這個超時模式能夠很好地工作。但是,還有一些微妙的情況需要考慮,并且坦白地說,對于Promise.race([ .. ]) 和Promise.all([ .. ]) 也都是如此。
- finally
它看起來可能類似于:
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
同時,我們可以構(gòu)建一個靜態(tài)輔助工具來支持查看(而不影響)Promise 的決議:
// polyfill安全的guard檢查
if (!Promise.observe) {
Promise.observe = function(pr, cb) {
// 觀察pr的決議
pr.then(function fulfilled(msg) {
// 安排異步回調(diào)(作為Job)
Promise.resolve(msg).then(cb);
}, function rejected(err) {
// 安排異步回調(diào)(作為Job)
Promise.resolve(err).then(cb);
});
// 返回最初的promise
return pr;
};
}
下面是如何在前面的超時例子中使用這個工具:
Promise.race([
Promise.observe(foo(), // 試著運行foo()
function cleanup(msg) {
// 在foo()之后清理,即使它沒有在超時之前完成
}),
timeoutPromise(3000) // 給它3秒鐘
])
這個輔助工具Promise.observe(..) 只是用來展示可以如何查看Promise 的完成而不對其產(chǎn)生影響。