你不知道的JavaScript(中卷)|Promise(二)

術(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ù)組。

  1. 超時競賽
    我們之前看到過這個例子,其展示了如何使用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([ .. ]) 也都是如此。

  1. 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)生影響。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容