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

什么是Promise

function add(getX, getY, cb) {
    var x, y;
    getX(function (xVal) {
        x = xVal;
        // 兩個都準備好了砚蓬?
        if (y != undefined) {
            cb(x + y); // 發(fā)送和
        }
    });
    getY(function (yVal) {
        y = yVal;
        // 兩個都準備好了铃剔?
        if (x != undefined) {
            cb(x + y); // 發(fā)送和
        }
    });
}
// fetchX() 和fetchY()是同步或者異步函數(shù)
add(fetchX, fetchY, function (sum) {
    console.log(sum); // 是不是很容易懂拾?
});
function add(xPromise, yPromise) {
    // Promise.all([ .. ])接受一個promise數(shù)組并返回一個新的promise捅儒,
    // 這個新promise等待數(shù)組中的所有promise完成
    return Promise.all([xPromise, yPromise])
        // 這個promise決議之后液样,我們?nèi)〉檬盏降腦和Y值并加在一起
        .then(function (values) {
            // values是來自于之前決議的promisei的消息數(shù)組
            return values[0] + values[1];
        });
}
// fetchX()和fetchY()返回相應(yīng)值的promise,可能已經(jīng)就緒巧还,
// 也可能以后就緒
add(fetchX(), fetchY())
    // 我們得到一個這兩個數(shù)組的和的promise
    // 現(xiàn)在鏈式調(diào)用 then(..)來等待返回promise的決議
    .then(function (sum) {
        console.log(sum); // 這更簡單鞭莽!
    });

fetchX() 和fetchY() 是直接調(diào)用的,它們的返回值(promiset锏弧)被傳給add(..)澎怒。這些promise代表的低層值的可用時間可能是現(xiàn)在或?qū)恚还茉鯓咏纂梗琾romise歸一保證了行為的一致性喷面。我們可以按照不依賴于時間的方式追蹤值X和Y。它們是未來值走孽。
第二層是add(..)(通過Promise.all([ .. ]))創(chuàng)建并返回的promise惧辈。我們通過調(diào)用then(..)等待這個promise。add(..)運算完成后磕瓷,未來值sum就準備好了盒齿,可以打印出來。我們把等待未來值X和Y的邏輯隱藏在了add(..)內(nèi)部。

在add(..)內(nèi)部县昂,Promise.all([..])調(diào)用創(chuàng)建了一個promise(這個promise等待promiseX和promiseY的決議)肮柜。鏈式調(diào)用.then(..)創(chuàng)建了另外一個promise。這個promise由return values[0] + values[1]這一行立即決議(得到加運算的結(jié)果)倒彰。因此审洞,鏈add(..)調(diào)用終止處的調(diào)用then(..)——在代碼結(jié)尾處——實際上操作的是返回的第二個promise,而不是由Promise.all([..])創(chuàng)建的第一個promise待讳。還有芒澜,盡管第二個then(..)后面沒有鏈接任何東西,但它實際上也創(chuàng)建了一個新的promise创淡,如果想要觀察或者使用它的話就可以看到痴晦。

Promise的決議結(jié)果也可能是拒絕而不是完成的。拒絕值和完成的Promise不一樣:完成值總是變成給出的琳彩,而拒絕值誊酌,通常稱為拒絕原因,可能是程序邏輯直接設(shè)置的露乏,也可能是從運行異常隱式得出的值碧浊。
通過Promise,調(diào)用then(..)實際上可以接受兩個函數(shù)瘟仿,第一個用于完成情況(如前所示)箱锐,第二個用于拒絕情況:

add(fetchX(), fetchY())
    .then(
    // 完成處理函數(shù)
    function (sum) {
        console.log(sum);
    },
    // 拒絕處理函數(shù)
    function (err) {
        console.error(err); // 煩!
    }
);

從外部看劳较,由于Promise封裝了依賴于時間的狀態(tài)——等待低層值的完成或拒絕驹止,所以Promise本身是與時間無關(guān)的。因此观蜗,Promise可以按照可預(yù)測的方式組成(組合)臊恋,而不是關(guān)心時序或低層的結(jié)果。
另外嫂便,一旦Promise決議捞镰,它就永遠保持在這個狀態(tài)。此時它就稱為了不變值毙替,可以根據(jù)需求多次查看岸售。

function foo(x) {
    // 可是做一些可能耗時的工作
    // 構(gòu)造并返回一個promise
    return new Promise(function (resolve, reject) {
        // 最終調(diào)用resolve(..)或者reject(..)
        // 這是這個promise的決議回調(diào)
    });
}
var p = foo(42);
bar(p);
baz(p);

new Promise( function(..){ .. } )模式通常稱為revealing constructor。傳入的函數(shù)會立即執(zhí)行(不會像then(..)中的回調(diào)一樣異步延遲)厂画,它有兩個參數(shù)凸丸,在本例中我們將其分別稱為resolve和reject。這些是promise的決議函數(shù)袱院。resolve(..)通常標識完成屎慢,而reject(..)則標識拒絕瞭稼。

你可能會猜測bar(..)和baz(..)的內(nèi)部實現(xiàn)或許如下:

function bar(fooPromise) {
    // 偵聽foo(..)完成
    fooPromise.then(
        function () {
            // foo(..)已經(jīng)完畢,所以執(zhí)行bar(..)的任務(wù)
        },
        function () {
            // 啊腻惠,foo(..)中出錯了环肘!
        }
    );
}
    // 對于baz(..)也是一樣

Promise決議并不一定要像前面將Promise作為未來值查看時一樣會涉及發(fā)送消息。它也可以只作為一種流程控制信號集灌,就像前面這段代碼中的用法一樣悔雹。
另外一種實現(xiàn)方式是:

function bar() {
    // foo(..)肯定已經(jīng)完成,所以執(zhí)行bar(..)的任務(wù)
}
function oopsBar() {
    // 啊欣喧,foo(..)中出錯了腌零,所以bar(..)沒有運行
}
// 對于baz()和oopsBaz()也是一樣
var p = foo(42);
p.then(bar, oopsBar);
p.then(baz, oopsBaz);

這里沒有把promise p傳給bar(..)和baz(..),而是使用promise控制bar(..)和baz(..)何時執(zhí)行唆阿,如果執(zhí)行的話益涧。最主要的區(qū)別在于錯誤處理部分。
在第一段代碼的方法里驯鳖,不論foo(..)成功與否闲询,bar(..)都會被調(diào)用。并且如果收到了foo(..)失敗的通知浅辙,它會親自處理自己的回退邏輯嘹裂。顯然,baz(..)也是如此摔握。
在第二段代碼中,bar(..)只有在foo(..)成功時才會被調(diào)用丁寄,否則就會調(diào)用oppsBar(..)氨淌。baz(..)也是如此。
這兩種方法本身并談不上對錯伊磺,只是各自適用于不同的情況盛正。
另外,兩端代碼都以使用promise p調(diào)用then(..)兩次結(jié)束屑埋。這個事實說明了前面的觀點豪筝,就是Promise(一旦決議)一直保持其決議結(jié)果(完成或拒絕)不變,可以按照需要多次查看摘能。

具有then方法的鴨子類型
在Promise領(lǐng)域续崖,一個重要的細節(jié)是如何確定某個值是不是真正的Promise⊥鸥悖或者更直接地說严望,它是不是一個行為方式類似于Promise的值?
既然Promise是通過new Promise(..)語法創(chuàng)建的逻恐,那你可以就認為可以通過p instanceof Promise來檢查像吻。但遺憾的是峻黍,這并不足以作為檢查方法,原因有許多拨匆。
其中最主要的是姆涩,Promise值可能是從其他瀏覽器窗口(iframe等)接受到的。這個瀏覽器窗口自己的Promise可能和當(dāng)前窗口/iframe的不同惭每,因此這樣的檢查無法識別Promise實例骨饿。
還有,庫或框架可能會選擇實現(xiàn)自己的Promise洪鸭,而不是使用原生ES6 Promise實現(xiàn)样刷。實際上,很有可能你是在早期根本沒有Promise實現(xiàn)的瀏覽器中使用由庫提供的Promise览爵。
識別Promise(或者行為類似于Promise的東西)就是定義某種稱為thenable的東西置鼻,將其定義為任何具有then(..)方法的對象和函數(shù)。我們認為蜓竹,任何這樣的值就是Promise一致的thenable箕母。
根據(jù)一個值的形態(tài)(具有哪些屬性)對這個值的類型做出一些假定。這種類型檢查(type check)一般用術(shù)語鴨子類型來表示——“如果它看起來像只鴨子俱济,叫起來像只鴨子嘶是,那它一定就是只鴨子”。于是蛛碌,對thenable值的鴨子類型檢測就大致類似于:

if (
    p !== null &&
    (
        typeof p === "object" ||
        typeof p === "function"
    ) &&
    typeof p.then === "function"
) {
    // 假定這是一個thenable!
}
else {
    // 不是thenable
}

如果你試圖使用恰好有then(..)函數(shù)的一個對象或函數(shù)值完成一個Promise聂喇,但并不希望它被當(dāng)作Promise或thenable,那就有點麻煩了蔚携,因為它會自動被識別為thenable希太,并被按照特定的規(guī)則處理。
即使你并沒有意識到這個值有then(..)函數(shù)也是這樣:

var o = { then: function () { } };
// 讓v [[Prototype]]-link到o
var v = Object.create(o);
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty("then"); // false

v看起來根本不像Promise或thenable酝蜒。它只是一個具有一些屬性的簡單對象誊辉。你可能只是想要像其他對象一樣發(fā)送這個值。
但是你不知道的是亡脑,v還[[Prototype]]連接到了另外一個對象o堕澄,而后者恰好具有一個then(..)屬性。所以thenable鴨子類型檢測會把v認作一個thenable霉咨。
甚至不需要是直接有意支持的:

Object.prototype.then = function(){};
Array.prototype.then = function(){};
var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];

v1和v2都會被認作thenable蛙紫。如果有任何其他代碼無意或惡意地給Object.prototype、Array.prototype或任何其他原生原型添加then(..)躯护,你無法控制也無法預(yù)測惊来。并且,如果指定的是不調(diào)用其參數(shù)作為回調(diào)的函數(shù)棺滞,那么如果有Promise決議到這樣的值裁蚁,就會永遠掛资冈ā!

Promise信任問題
先回顧一下只用回調(diào)編碼的信任問題枉证。那一個回調(diào)傳入工具foo(..)時可能出現(xiàn)如下問題:

  • 調(diào)用回調(diào)過早矮男;
  • 調(diào)用回調(diào)過晚(或不被調(diào)用);
  • 調(diào)用回調(diào)次數(shù)過少或過多室谚;
  • 未能傳遞所需的環(huán)境和參數(shù)毡鉴;
  • 吞掉可能出現(xiàn)的錯誤和異常;
    Promise的特性就是專門用來為這些問題提供一個有效的可復(fù)用的答案秒赤。

調(diào)用過早
這個問題主要就是擔(dān)心代碼是否會引入類似Zalgo這樣的副作用猪瞬。在這類問題中,一個任務(wù)有時同步完成入篮,有時異步完成陈瘦,這可能會導(dǎo)致競態(tài)條件。
根據(jù)定義潮售,Promise就不必擔(dān)心這種問題痊项,因為即使是立即完成的Promise(類似于new Promise(function(resolve){resolve(42);}))也無法被同步觀察到。
也就是說酥诽,對一個Promise調(diào)用then(..)的時候鞍泉,即使這個Promise已經(jīng)決議,提供給then(..)的回調(diào)也總會被異步調(diào)用肮帐。
不再需要插入你自己的setTimeout(..,0) hack咖驮,Promise會自動防止Zalgo出現(xiàn)。

調(diào)用過晚
和前面一點類似训枢,Promise創(chuàng)建對象調(diào)用resolve(..)或reject(..)時游沿,這個Promise的then(..)注冊的觀察回調(diào)就會被自動調(diào)度“估可以確信,這些被調(diào)度的回調(diào)在下一個異步事件點上一定會被觸發(fā)袋坑。
同步查看是不可能的仗处,所以一個同步任務(wù)鏈無法以這種方式運行來實現(xiàn)按照預(yù)期有效延遲另一個回調(diào)的發(fā)生。也就是說枣宫,一個Promise決議后婆誓,這個Promise上所有的通過then(..)注冊的回調(diào)都會在下一個異步時機點上依次被立即調(diào)用。這些回調(diào)中的任意一個都無法影響或延誤對其他回調(diào)的調(diào)用也颤。

p.then(function () {
    p.then(function () {
        console.log("C");
    });
    console.log("A");
});
p.then(function () {
    console.log("B");
});
    // A B C

這里洋幻,“C”無法打斷或搶占“B”,這是因為Promise的運作方式翅娶。

Promise調(diào)度技巧
但是文留,還有很重要的一點需要指出好唯,有很多調(diào)度的細微差別。在這種情況下燥翅,兩個獨立Promise上鏈接的回調(diào)的相對順序無法可靠預(yù)測骑篙。
如果兩個promise p1和p2都已經(jīng)決議,那么p1.then(..);和p2.then(..)應(yīng)該最終會先調(diào)用p1的回調(diào)森书,然后是p2的那些靶端。但還有一些細微的場景可能不是這樣的:

var p3 = new Promise(function (resolve, reject) {
    resolve("B");
});
var p1 = new Promise(function (resolve, reject) {
    resolve(p3);
});
p2 = new Promise(function (resolve, reject) {
    resolve("A");
});
p1.then(function (v) {
    console.log(v);
});
p2.then(function (v) {
    console.log(v);
});
// A B <-- 而不是像你可能認為的B A

p1不是用立即值而是用另一個promise p3決議,后者本身決議為值“B”凛膏。規(guī)定的行為時把p3展開到p1杨名,但是是異步地展開。所以猖毫,在異步任務(wù)隊列中台谍,p1的回調(diào)排在p2的回調(diào)之后。
要避免這樣的細微區(qū)別帶來的噩夢鄙麦,你永遠都不應(yīng)該依賴于不同Promise間回調(diào)的順序和調(diào)度典唇。實際上,好的編碼實踐方案根本不會讓多個回調(diào)的順序有絲毫影響胯府,可能的話就要避免介衔。

回調(diào)未調(diào)用
首先,沒有任何東西(甚至是JavaScript錯誤)能阻止Promise向你通知它的決議(如果它決議了的話)骂因。如果你對一個Promise注冊了一個完成回調(diào)和一個拒絕回調(diào)炎咖,那么Promise在決議時總是會調(diào)用其中的一個。
但是寒波,如果Promise本身永遠不被決議呢乘盼?即使這樣,Promise也提供了解決方案俄烁,其使用了一種稱為競態(tài)的高級抽象機制:

// 用于超時一個Promise的工具
function timeoutPromise(delay) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            reject("Timeout!");
        }, delay);
    });
}
// 設(shè)置foo()超時
Promise.race([
    foo(), // 試著開始foo()
    timeoutPromise(3000) // 給它3秒鐘
])
    .then(
    function () {
        // foo(..)及時完成绸栅!
    },
    function (err) {
        // 或者foo()被拒絕,或者只是沒能按時完成
        // 查看err來了解是哪種情況
    }
    );

關(guān)于這個Promise超時模式還有更多細節(jié)需要考量页屠,后面我們會深入討論粹胯。
很重要的一點是,我們可以保證一個foo()有一個輸出信號辰企,防止其永久掛住程序风纠。

調(diào)用次數(shù)過少或過多
根據(jù)定義,回調(diào)被調(diào)用的正確次數(shù)應(yīng)該是1牢贸≈窆郏“過少”的情況就是調(diào)用0次,和前面解釋過的“未被”調(diào)用是同一種情況。
“過多”的情況很容易解釋臭增。Promise的定義方式使得它只能被決議一次屏箍。如果出于某種原因沪摄,Promise創(chuàng)建代碼試圖調(diào)用resolve(..)或reject(..)多次,或者試圖兩者都調(diào)用,那么這個Promise將只會接受第一次決議晶乔,并默默地忽略任何后續(xù)調(diào)用赛蔫。
由于Promise只能被決議一次逝嚎,所以任何通過then(..)注冊的(每個)回調(diào)就只會被調(diào)用一次僻弹。
當(dāng)然,如果你把同一個回調(diào)注冊了不止一次(比如p.then(f)并炮;p.then(f)默刚;),那它被調(diào)用的次數(shù)就會和注冊次數(shù)相同逃魄。響應(yīng)函數(shù)只會被調(diào)用一次荤西,但這個保證并不能預(yù)防你搬起石頭砸自己的腳。

未能傳遞參數(shù)/環(huán)境值
Promise至多只能有一個決議值(完成或拒絕)伍俘。
如果你沒有用任何值顯式?jīng)Q議邪锌,那么這個值就是undefined,這是JavaScript常見的處理方式癌瘾。但不管這個值是什么觅丰,無論當(dāng)前或未來,它都會被傳給所有注冊的(且適當(dāng)?shù)耐瓿苫蚓芙^)回調(diào)妨退。
還有一點需要清楚:如果使用多個參數(shù)調(diào)用resolve(..)或者reject(..)妇萄,第一個參數(shù)之后的所有參數(shù)都會被默默忽略。這看起來似乎違背了我們前面介紹的保證咬荷,但實際上并沒有冠句,因為這是對Promise機制的無效使用。對于這組API的其他無效使用(比如多次重讀調(diào)用resolve(..))幸乒,也是類似的保護處理懦底,所以這里的Promise行為是一致的。
如果要傳遞多個值罕扎,你就必須要把它們封裝在單個值中傳遞基茵,比如通過一個數(shù)組或?qū)ο蟆?br> 對環(huán)境來說,JavaScript中的函數(shù)總是保持其定義所在的作用域的閉包壳影,所以它們當(dāng)然可以繼續(xù)訪問你提供的環(huán)境狀態(tài)。當(dāng)然弥臼,對于只用回調(diào)的設(shè)計也是這樣的宴咧,因此這并不是Promise特有的優(yōu)點——但不管怎樣,這仍然是我們可以依靠的一個保證径缅。

吞掉錯誤或異常
基本上掺栅,這部分是上個要點的再次說明烙肺。如果拒絕一個Promise并給出一個理由(也就是一個出錯消息),這個值就會被傳給拒絕回調(diào)氧卧。
如果在Promise的創(chuàng)建過程中或在查看其決議結(jié)果過程中的任何時間點上出現(xiàn)了一個JavaScript異常錯誤桃笙,比如一個TypeError或ReferenceError,那這個異常就會被捕捉沙绝,并且會使這個Promise被拒絕:

var p = new Promise(function (resolve, reject) {
    foo.bar(); // foo未定義搏明,所以會出錯!
    resolve(42); // 永遠不會到達這里 :(
});
p.then(
    function fulfilled() {
        // 永遠不會到達這里 :(
    },
    function rejected(err) {
        // err將會是一個TypeError異常對象來自foo.bar()這一行
    }
);

foo.bar()中發(fā)生的JavaScript異常導(dǎo)致了Promise拒絕闪檬,你可以捕捉并對其作出響應(yīng)星著。
這是一個重要的細節(jié),因為其有效解決了另外一個潛在的Zalgo風(fēng)險粗悯,即出錯可能會引起同步響應(yīng)虚循,而不出錯則會是異步的。Promise甚至把JavaScript異常也變成了異步行為样傍,進而極大降低了競態(tài)條件出現(xiàn)的可能横缔。
但是,如果Promise完成后再查看結(jié)果時(then(..)注冊的回調(diào)中)出現(xiàn)了JavaScript異常錯誤會怎樣呢衫哥?即使這些異常不會被丟棄茎刚,但你會發(fā)現(xiàn),對它們的處理方式還是有點出乎意料炕檩,需要進行一些深入研究才能理解:

var p = new Promise(function (resolve, reject) {
    resolve(42);
});
p.then(
    function fulfilled(msg) {
        foo.bar();
        console.log(msg); // 永遠不會到達這里 :(
    },
    function rejected(err) {
        // 永遠也不會到達這里 :(
    }
);

這看起來像是foo.bar()產(chǎn)生的異常真的被吞掉了斗蒋。別擔(dān)心,實際上并不是這樣笛质。但是這里有一個深藏的問題泉沾,就是我們沒有偵聽到它。p.then(..)調(diào)用本身返回了另外一個promise妇押,正是這個promise將會因TypeError異常而被拒絕跷究。
為什么它不是簡單地調(diào)用我們定義的錯誤處理函數(shù)呢?表面上的邏輯應(yīng)該是這樣啊敲霍。如果這樣的話就違背了Promise的一條基本原則俊马,即Promise一旦決議就不可再變。p已經(jīng)完成為值42肩杈,所以之后查看p的決議時柴我,并不能因為出錯就把p再變?yōu)橐粋€拒絕。
除了違背原則之外扩然,這樣的行為也會造成嚴重的損害艘儒。因為假如這個promise p有多個then(..)注冊的回調(diào)的話,有些回調(diào)會被調(diào)用,而有些則不會界睁,情況會非常不透明觉增,難以解釋。

是可信任的Promise嗎
Promise并沒有完全擺脫回調(diào)翻斟,它們只是改變了傳遞回調(diào)的位置逾礁。我們并不是把回調(diào)傳遞給foo(..),而是從foo(..)得到某個東西(外觀上看是一個真正的Promise)访惜,然后把回調(diào)傳給這個東西嘹履。
但是,為什么這就比單純使用回調(diào)更值得信任呢疾牲?如何能夠確定返回的這個東西實際上就是一個可信任的Promise呢植捎?這難道不是一個(脆弱的)紙牌屋,在里面只能信任我們已經(jīng)信任的阳柔?
關(guān)于Promise的很重要但是常常被忽略的一個細節(jié)是焰枢,Promise對這個問題已經(jīng)有一個解決方案。包含在原生ES6 Promise實現(xiàn)中的解決方案就是Promise.resolve(..)舌剂。
如果向Promise.resolve(..)傳遞一個非Promise济锄、非thenable的立即值,就會得到一個用這個值填充的promise霍转。下面這種情況下荐绝,promise p1和promise p2的行為是完全一樣的。

var p1 = new Promise(function (resolve, reject) {
    resolve(42);
});
var p2 = Promise.resolve(42);

而如果向Promise.resolve(..)傳遞一個真正的Promise避消,就只會返回同一個promise:

var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true

更重要的是低滩,如果向Promise.resolve(..)傳遞了一個非Promise的thenable值,前者就會試圖展開這個值岩喷,而且展開過程會持續(xù)到提取出一個具體的非類Promise的最終值恕沫。

var p = {
    then: function (cb) {
        cb(42);
    }
};
// 這可以工作,但只是因為幸運而已
p.then(
    function fulfilled(val) {
        console.log(val); // 42
    },
    function rejected(err) {
        // 永遠不會到達這里
    }
);

這個p是一個thenable纱意,但并不是一個真正的Promise婶溯。幸運的是,和絕大多數(shù)值一樣偷霉,它是可追蹤的迄委。但是,如果得到的是如下這樣的值又會怎樣呢:

var p = {
    then: function (cb, errcb) {
        cb(42);
        errcb("evil laugh");
    }
};
p.then(
    function fulfilled(val) {
        console.log(val); // 42
    },
    function rejected(err) {
        // 啊类少,不應(yīng)該運行叙身!
        console.log(err); // 邪惡的笑
    }
);

這個p是一個thenable,但是其行為和promise并不完全一致硫狞。這是惡意的嗎信轿?還只是因為它不知道Promise應(yīng)該如何運作赞警?說實話,這并不重要虏两。不管是哪種情況,它都是不可信任的世剖。
盡管如此定罢,我們還是都可以把這些版本的p傳給Promise.resolve(..),然后就會得到期望中的規(guī)范化后的安全結(jié)果:

Promise.resolve(p)
    .then(
    function fulfilled(val) {
        console.log(val); // 42
    },
    function rejected(err) {
        // 永遠不會到達這里
    }
    );

Promise.resolve(..)可以接受任何thenable旁瘫,將其解封為它的非thenable值祖凫。從Promise.resolve(..)得到的是一個真正的Promise,是一個可以信任的值酬凳。如果你傳入的已經(jīng)是真正的Promise惠况,那么你得到的就是它本身,所以通過Promise.resolve(..)過濾來獲得可信任性完全沒有壞處宁仔。
假設(shè)我們要調(diào)用一個工具foo(..)稠屠,且并不確定得到的返回值是否是一個可信任的行為良好的Promise,但我們可以知道它至少是一個thenable翎苫。Promise.resolve(..)提供了可信任的Promise封裝工具权埠,可以鏈接使用:

// 不要只是這么做:
foo(42)
    .then(function (v) {
        console.log(v);
    });
// 而要這么做:
Promise.resolve(foo(42))
    .then(function (v) {
        console.log(v);
    });

對于用Promise.resolve(..)為所有函數(shù)的返回值(不管是不是thenable)都封裝一層。另一個好處是煎谍,這樣做很容易把函數(shù)調(diào)用規(guī)范為定義良好的異步任務(wù)攘蔽。如果foo(42)有時會返回一個立即值,有時會返回Promise呐粘,那么Promise.resolve(foo(42))就能夠保證總會返回一個Promise結(jié)果满俗。而且避免Zalgo就能得到更好的代碼。

鏈式流
盡管我們之前對此有過幾次暗示作岖,但Promise并不只是一個單步執(zhí)行this-then-that操作的機制唆垃。當(dāng)然,那是構(gòu)成部件鳍咱,但是我們可以把多個Promise連接到一起以表示一系列異步步驟降盹。
這種方式可以實現(xiàn)的關(guān)鍵在于以下兩個Promise固有行為特性:

  • 每次你對Promise調(diào)用then(..),它都會創(chuàng)建并返回一個新的Promise谤辜,我們可以將其鏈接起來蓄坏;
  • 不管從then(..)調(diào)用的完成回調(diào)(第一個參數(shù))返回的值是什么,它都會被自動設(shè)置為被鏈接Promise(第一點中的)的完成丑念。
var p = Promise.resolve(21);
var p2 = p.then(function (v) {
    console.log(v); // 21
    // 用值42填充p2
    return v * 2;
});
// 連接p2
p2.then(function (v) {
    console.log(v); // 42
});

但是涡戳,如果必須創(chuàng)建一個臨時變量p2(或p3等),還是有一點麻煩的:

var p = Promise.resolve(21);
p.then(function (v) {
    console.log(v); // 21
    // 用值42完成連接的promise
    return v * 2;
})
    // 這里是鏈接的promise
    .then(function (v) {
        console.log(v); // 42
    });

現(xiàn)在第一個then(..)就是異步序列中的第一步脯倚,第二個then(..)就是第二步渔彰。這可以一直任意擴展下去嵌屎。只要保持把先前的then(..)連到自動創(chuàng)建的每一個Promise即可。
但這里還漏掉了一些東西恍涂。如果需要步驟2等待步驟1異步來完成一些事情怎么辦宝惰?我們使用了立即返回return語句,這會立即完成鏈接的promise再沧。
使Promise序列真正能夠在每一步有異步能力的關(guān)鍵是尼夺,回憶一下當(dāng)傳遞給Promise.resolve(..)的是一個Promise或thenable而不是最終值時的運作方式。Promise.resolve(..)會直接返回接收到的真正Promise炒瘸,或展開接收到的thenable值淤堵,并在持續(xù)展開thenable的同時遞歸地前進。
從完成(或拒絕)處理函數(shù)返回thenable或者Promise的時候也會發(fā)生同樣的展開:

var p = Promise.resolve(21);
p.then(function (v) {
    console.log(v); // 21
    // 創(chuàng)建一個promise并將其返回
    return new Promise(function (resolve, reject) {
        // 用值42填充
        resolve(v * 2);
    });
})
    .then(function (v) {
        console.log(v); // 42
    });

雖然我們把42封裝到了返回的promise中顷扩,但它仍然會被展開并最終成為鏈接的promise的決議拐邪,因此第二個then(..)得到的仍然是42。如果我們向封裝的promise引入異步隘截,一切都仍然會同樣工作:

var p = Promise.resolve(21);
p.then(function (v) {
    console.log(v); // 21
    // 創(chuàng)建一個promise并返回
    return new Promise(function (resolve, reject) {
        // 引入異步扎阶!
        setTimeout(function () {
            // 用值42填充
            resolve(v * 2);
        }, 100);
    });
})
    .then(function (v) {
        // 在前一步中的100ms延遲之后運行
        console.log(v); // 42
    });

為了進一步闡釋鏈接,讓我們把延遲Promise創(chuàng)建(沒有決議消息)過程一般化到一個工具中技俐,以便在多個步驟中復(fù)用:

function delay(time) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, time);
    });
}
delay(100) // 步驟1
    .then(function STEP2() {
        console.log("step 2 (after 100ms)");
        return delay(200);
    })
    .then(function STEP3() {
        console.log("step 3 (after another 200ms)");
    })
    .then(function STEP4() {
        console.log("step 4 (next Job)");
        return delay(50);
    })
    .then(function STEP5() {
        console.log("step 5 (after another 50ms)");
    })
...

調(diào)用delay(200)創(chuàng)建了一個將在200ms后完成的promise乘陪,然后我們從第一個then(..)完成回調(diào)中返回這個promise,這會導(dǎo)致第二個then(..)的promise等待這個200ms的promise雕擂。

如前所述啡邑,嚴格地說,這個交互過程總有兩個promise:200ms延遲promise井赌,和第二個then(..)鏈接到的那個鏈接promise谤逼。但是你可能已經(jīng)發(fā)現(xiàn)了,在腦海中把這兩個promise合二為一之后更好理解仇穗,因為promise機制已經(jīng)自動為你把它們的狀態(tài)合并在了一起流部。這樣一來,可以把return delay(200)看作是創(chuàng)建了一個promise纹坐,并用其替換了前面返回的鏈接promise枝冀。

但說實話,沒有消息傳遞的延遲序列對于Promise流程控制來說并不是一個很有用的示例耘子。這里不用定時器果漾,而是構(gòu)造Ajax請求:

// 假定工具ajax( {url}, {callback} )存在
// Promise-aware ajax
function request(url) {
    return new Promise(function (resolve, reject) {
        // ajax(..)回調(diào)應(yīng)該是我們這個promise的resolve(..)函數(shù)
        ajax(url, resolve);
    });
}

我們首先定義一個工具request(..),用來構(gòu)造一個表示ajax(..)調(diào)用完成的promise:

request("http://some.url.1/")
    .then(function (response1) {
        return request("http://some.url.2/?v=" + response1);
    })
    .then(function (response2) {
        console.log(response2);
    });

利用返回Promise的request(..)谷誓,我們通過使用第一個URL調(diào)用它來創(chuàng)建鏈接中的第一步绒障,并且把返回的promise與第一個then(..)鏈接起來。
response1一返回捍歪,我們就使用這個值構(gòu)造第二個URL户辱,并發(fā)出第二個request(..)調(diào)用鸵钝。第二個request(..)的promise返回,以便異步流控制中的第三步等待這個Ajax調(diào)用完成庐镐。最后恩商,response2一返回,我們就立即打出結(jié)果必逆。
我們構(gòu)建的這個Promise鏈不僅是一個表達多步異步序列的流程控制痕届,還是一個從一個步驟到下一個步驟傳遞消息的消息通道。
如果這個Promise鏈中的某個步驟出錯了怎么辦末患?錯誤和異常是基于每個Promise的,這意味著可能在鏈的任意位置捕捉到這樣的錯誤锤窑,而這個捕捉動作在某種程序上就相當(dāng)于在這一位置將整條鏈“重置”回了正常運作:

// 步驟1:
request("http://some.url.1/")
    // 步驟2:
    .then(function (response1) {
        foo.bar(); // undefined璧针,出錯!
        // 永遠不會到達這里
        return request("http://some.url.2/?v=" + response1);
    })
    // 步驟3:
    .then(
    function fulfilled(response2) {
        // 永遠不會到達這里
    },
    // 捕捉錯誤的拒絕處理函數(shù)
    function rejected(err) {
        console.log(err);
        // 來自foo.bar()的錯誤TypeError
        return 42;
    }
    )
    // 步驟4:
    .then(function (msg) {
        console.log(msg); // 42
    });

第2步出錯后渊啰,第3步的拒絕處理函數(shù)會捕捉到這個錯誤探橱。拒絕處理函數(shù)的返回值(這段代碼中是42),如果有的話绘证,會用來完成交給下一個步驟與(第4步)的promise隧膏,這樣,這個鏈現(xiàn)在就回到了完成狀態(tài)嚷那。

正如之前討論過的胞枕,當(dāng)從完成處理函數(shù)返回一個promise時,它會被展開并有可能延遲下一個步驟魏宽。從拒絕處理函數(shù)返回promise也是如此腐泻,因此如果在第3步返回的不是42而是一個promise的話,這個promise可能會延遲第4步队询。調(diào)用then(..)時的完成處理函數(shù)或拒絕處理函數(shù)如果拋出異常派桩,都會導(dǎo)致(鏈中的)下一個promise因這個異常而立即被拒絕。

如果你調(diào)用promise的then(..)蚌斩,并且只傳入一個完成處理函數(shù)铆惑,一個默認拒絕處理函數(shù)就會頂替上來:

var p = new Promise(function (resolve, reject) {
    reject("Oops");
});
var p2 = p.then(
    function fulfilled() {
        // 永遠不會達到這里
    }
    // 假定的拒絕處理函數(shù),如果省略或者傳入任何非函數(shù)值
    // function(err) {
    // throw err;
    // }
);

如你所見送膳,默認拒絕處理函數(shù)只是把錯誤重新拋出员魏,這最終會使得p2(鏈接的promise)用同樣的錯誤理由拒絕。從本質(zhì)上說肠缨,這使得錯誤可以繼續(xù)沿著Promise鏈傳播下去逆趋,直到遇到顯式定義的拒絕處理函數(shù)。
如果沒有給then(..)傳遞一個適當(dāng)有效的函數(shù)作為完成處理函數(shù)參數(shù)晒奕,還是會有作為替代的一個默認處理函數(shù):

var p = Promise.resolve(42);
p.then(
    // 假設(shè)的完成處理函數(shù)闻书,如果省略或者傳入任何非函數(shù)值
    // function(v) {
    // return v;
    // }
    null,
    function rejected(err) {
        // 永遠不會到達這里
    }
);

你可以看到名斟,默認的完成處理函數(shù)只是把接收到的任何傳入值傳遞給下一個步驟(Promise)而已。

then(null,function(err){ .. })這個模式——只處理拒絕(如果有的話)魄眉,但又把完成值傳遞下去——有一個縮寫形式的API:catch(function(err){ .. })砰盐。

簡單總結(jié)一下使鏈式流程控制可行的Promise固有特性:

  • 調(diào)用Promise的then(..)會自動創(chuàng)建一個新的Promise從調(diào)用返回。
  • 在完成或拒絕處理函數(shù)內(nèi)部坑律,如果返回一個值或拋出一個異常岩梳,新返回的(可鏈接的)Promise就相應(yīng)地決議。
  • 如果完成或拒絕處理函數(shù)返回一個Promise晃择,它將會被展開冀值,這樣一來,不管它的決議值是什么宫屠,都會成為當(dāng)前then(..)返回的鏈接Promise的決議值列疗。
別人想讓你看到什么,你的能力又能讓你看到什么
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浪蹂,一起剝皮案震驚了整個濱河市抵栈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坤次,老刑警劉巖古劲,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缰猴,居然都是意外死亡产艾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門滑绒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胰舆,“玉大人,你說我怎么就攤上這事蹬挤「苛” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵焰扳,是天一觀的道長倦零。 經(jīng)常有香客問我,道長吨悍,這世上最難降的妖魔是什么扫茅? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮育瓜,結(jié)果婚禮上葫隙,老公的妹妹穿的比我還像新娘。我一直安慰自己躏仇,他們只是感情好恋脚,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布腺办。 她就那樣靜靜地躺著,像睡著了一般糟描。 火紅的嫁衣襯著肌膚如雪怀喉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天船响,我揣著相機與錄音躬拢,去河邊找鬼。 笑死见间,一個胖子當(dāng)著我的面吹牛聊闯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播米诉,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼馅袁,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了荒辕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤犹褒,失蹤者是張志新(化名)和其女友劉穎抵窒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叠骑,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡李皇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了宙枷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掉房。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖慰丛,靈堂內(nèi)的尸體忽然破棺而出卓囚,到底是詐尸還是另有隱情,我是刑警寧澤诅病,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布哪亿,位于F島的核電站,受9級特大地震影響贤笆,放射性物質(zhì)發(fā)生泄漏蝇棉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一芥永、第九天 我趴在偏房一處隱蔽的房頂上張望篡殷。 院中可真熱鬧,春花似錦埋涧、人聲如沸板辽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戳气。三九已至链患,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瓶您,已是汗流浹背麻捻。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呀袱,地道東北人贸毕。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像夜赵,于是被迫代替她去往敵國和親明棍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容