Promise
Promise對象是一個代理對象吼鳞。它接受你傳入的 executor (執(zhí)行器)作為入?yún)ⅲ试S你把異步任務的成功和失敗分別綁定到對應的處理方法上兄渺。一個 Promise 實例有三種狀態(tài):
- pending 狀態(tài)择浊,表示進行中。這是 Promise 實例創(chuàng)建后的一個初始態(tài)刹碾;
- fulfilled 狀態(tài),表示成功完成座柱。這是我們在執(zhí)行器中調(diào)用 resolve 后迷帜,達成的狀態(tài);
- rejected 狀態(tài)色洞,表示操作失敗戏锹、被拒絕。這是我們在執(zhí)行器中調(diào)用 reject后火诸,達成的狀態(tài)锦针;
【狀態(tài)切換機制】Promise實例的狀態(tài)是可以改變的,但它只允許被改變一次。 當我們的實例狀態(tài)從 pending 切換為 rejected 后奈搜,就無法再扭轉(zhuǎn)為 fulfilled悉盆,反之同理。當 Promise 的狀態(tài)為 resolved 時馋吗,會觸發(fā)其對應的 then 方法入?yún)⒗锏?onfulfilled 函數(shù)舀瓢;當 Promise 的狀態(tài)為 rejected 時,會觸發(fā)其對應的 then 方法入?yún)⒗锏?onrejected 函數(shù)耗美。
Promise解決的痛點
對于回調(diào)地獄的引發(fā)的問題,我們需要一種更加友好的代碼組織方式航缀,解決異步嵌套的問題商架。
于是 Promise 規(guī)范誕生了,并且在業(yè)界有了很多實現(xiàn)來解決回調(diào)地獄的痛點芥玉。比如業(yè)界著名的 Q 和 bluebird 蛇摸,bluebird 甚至號稱運行最快的類庫。
Promise對象現(xiàn)已在ECMAScript 2015中作為JavaScript的標準內(nèi)置對象提供灿巧,這個對象根據(jù) Promise A+ 規(guī)范實現(xiàn)赶袄。(Promise規(guī)范有很多,如Promise/A抠藕,Promise/B饿肺,Promise/D 以及 Promise/A的升級版 Promise/A+,最終ES6采用了Promise/A+規(guī)范)
- 回調(diào)嵌套 -> 理解問題盾似,缺乏順序性
new Promise(請求1)
.then(請求2(請求結(jié)果1))
.then(請求3(請求結(jié)果2))
.then(請求4(請求結(jié)果3))
.then(請求5(請求結(jié)果4))
.catch(處理異常(異常信息))
對比Promise寫法和嵌套回調(diào)寫法敬辣,Promise鏈以順序的方式表達異步流,有助于我們的大腦更好的計劃和維護異步JavaScript代碼零院,并且能夠在外層捕獲異步函數(shù)的異常信息溉跃。
- 控制反轉(zhuǎn) -> 信任問題
如果我們能夠把控制反轉(zhuǎn)再反轉(zhuǎn)回來,會怎樣呢告抄?如果我們不把自己程序的continuation傳給第三方撰茎,而是希望第三方給我們提供了解其任務何時結(jié)束的能力,然后由我們自己的代碼來決定下一步做什么打洼,那將會怎樣呢龄糊?這種范式就稱為Promise。
Promise封裝了依賴于時間的狀態(tài)——等待底層值的完成或拒絕募疮,所以Promise本身是與時間無關(guān)的绎签。因此,Promise可以按照可預測的方式組合酝锅,而不用關(guān)心時序或者底層的結(jié)果诡必。
Promise是一種封裝和組合未來值的易于復用的機制。
Promise的決議也可以看做是一種在異步任務中作為兩個或更多步驟的流程控制機制。
?? 場景:要調(diào)用一個函數(shù)foo()執(zhí)行某個任務爸舒,期望通過某種方式在foo()執(zhí)行完成時得到通知
??? 思考:在典型的JavaScript場景中蟋字,如果需要偵聽某個通知,就會使用事件扭勉。需實現(xiàn)對foo()發(fā)出的一個完成事件的偵聽鹊奖。
- 使用回調(diào),通知就是任務(foo(...))調(diào)用的回調(diào)涂炎。
- 使用Promise忠聚,這個關(guān)系就反轉(zhuǎn)過來了,偵聽來自foo(..)的事件唱捣,然后在得到通知的時候做相關(guān)處理两蟀。
- 顯式地創(chuàng)建并返回一個事件訂閱對象:
function foo(x){
// ...do something
// 構(gòu)造一個listener處理
return listener;
}
var evt = foo(42);
evt.on("completion", function(){
// 可以進行下一步
})
evt.on("failure", function(){
// foo(..)中出錯了
})
Promise模式構(gòu)建的最重要的特性,就是解決了部分信任問題:
- 調(diào)用過早:即使這個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)度。也就是說巡社,一個Promise決議后膛堤,這個Promise上所有通過then(..)注冊的回調(diào)都會在下一個異步時機點上依次被立即調(diào)用。
- 回調(diào)未調(diào)用 & 調(diào)用次數(shù)過少(0次):如果你對一個Promise注冊了一個完成回調(diào)和一個拒絕回調(diào)晌该,那么Promise在決議時總是會調(diào)用其中的一個骑祟。即使回調(diào)本身包含JavaScript錯誤,也不會被吞掉气笙。
如果Promise本身永遠不被決議次企,Promise提供超時模式來解決。 - 調(diào)用次數(shù)過多(>1次):Promise的定義方式使得其只能被決議一次潜圃,所有通過then(..)注冊的回調(diào)都只會被調(diào)一次缸棵。Promise只接受第一次決議,并默默地忽略任何后續(xù)調(diào)用谭期。
- 未能傳遞參數(shù)/環(huán)境值:如果沒有用任何值顯式?jīng)Q議堵第,那這個值就是undefined,會被傳給所有注冊的回調(diào)隧出。
- 吞掉錯誤或異常:在Promise創(chuàng)建過程或查看其決議結(jié)果過程中的任何時間點上出現(xiàn)JavaScript異常踏志,這個異常都會被捕獲,并且會使這個Promise被拒絕胀瞪。
var p = new Promise(function(resolve, reject){
resolve(42);
});
p.then(function fulfilled(msg){
foo.bar();
console.log(msg); // 永遠不會到達這里
}, function rejected(err){
console.log(err); // 永遠不會到達這里
}).then(function fulfilled(msg){
console.log('....'+msg); // 永遠不會到達這里
}, function rejected(err){
console.log('....')
console.log(err); // 到達這里
})
Promise并沒有完成擺脫回調(diào)针余,只是改變了傳遞回調(diào)的位置饲鄙。并沒有把回調(diào)傳給foo(..),而是從foo(..)獲得某個東西(Promise)圆雁,然后把回調(diào)傳給他忍级。
??? Q:為什么這就比單純的使用回調(diào)更值得信任呢?如何確定返回的這個東西實際上就是一個可信任的Promise伪朽?
?? A:Promise對這個問題已經(jīng)有一個解決方案:原生ES6 Promise實現(xiàn)中的解決方案就是 Promise.resolve() 轴咱。 可接受任何thenable,得到一個真正的Promise烈涮。如果傳入的已經(jīng)是真正的Promise朴肺,將得到其本身。
Promise常見方法及其作用
類方法
JavaScript中的類(對象)方法可以認為是靜態(tài)方法(即:不需要實例化就可以使用的方法)
-
Promise.all(iterable)
:這個方法返回一個新的 promise 對象坚洽,該 promise 對象在 iterable 參數(shù)對象里所有的 promise 對象都成功的時候才會觸發(fā)成功戈稿,一旦有任何一個 iterable 里面的 promise 對象失敗則立即觸發(fā)該 promise 對象的失敗。 -
Promise.race(iterable)
:當 iterable 參數(shù)里的任意一個子 promise 被成功或失敗后酪术,父 promise 馬上也會用子 promise 的成功返回值或失敗詳情作為參數(shù)調(diào)用父 promise 綁定的相應處理函數(shù),并返回該 promise 對象翠储。 -
Promise.reject(reason)
: 返回一個狀態(tài)為失敗的Promise對象绘雁,并將給定的失敗信息傳遞給對應的處理方法。 -
Promise.resolve(value)
:它返回一個 Promise 對象援所,但是這個對象的狀態(tài)由你傳入的value決定庐舟,情形分以下三種:
- 如果傳入的是一個帶有 then 方法的對象(我們稱為 thenable 對象),返回的Promise對象會跟隨這個thenable對象住拭,采用它的最終狀態(tài)( resolved/rejected/pending/settled)挪略;
- 如果傳入的value本身就是Promise對象,則該對象作為Promise.resolve方法的返回值返回滔岳;
- 其他情況以該值為成功狀態(tài)返回一個Promise對象杠娱;
// 如果傳入的 value 本身就是 Promise 對象,則該對象作為 Promise.resolve 方法的返回值返回谱煤。
function fn(resolve){
setTimeout(function(){
resolve(123);
},3000);
}
let p0 = new Promise(fn);
let p1 = Promise.resolve(p0);
console.log(p0 === p1); // 返回為true摊求,返回的 Promise 即是 入?yún)⒌?Promise 對象。
實例方法
實例方法刘离,是指創(chuàng)建Promise實例后才能使用的方法室叉,即:被添加到原型鏈 Promise.prototype
上的方法。
-
Promise.prototype.then
實例方法硫惕,為Promise注冊回調(diào)茧痕,fn(value){}
其中value是上一個任務的返回結(jié)果。如果我們的后續(xù)任務是異步任務的話恼除,必須return一個新的promise對象踪旷;如果后續(xù)任務是同步任務,只需return一個結(jié)果即可。
then 中的函數(shù)一定要 return 一個結(jié)果或者一個新的 Promise 對象埃脏,才可以讓之后的then 回調(diào)接收搪锣。 -
Promise.prototype.catch
捕獲異常,可以捕獲到前面回調(diào)中可能拋出的異常彩掐。
Promise A+
規(guī)范規(guī)定:每個Promise實例中返回的都應該是一個Promise實例或thenable對象构舟。基于這個特性堵幽,能夠?qū)崿F(xiàn)類似于同步的鏈式調(diào)用狗超。
new Promise((resolve, reject) => {
// a()
resolve(2)
}).then(value => {
a();
console.log(value);
}).catch(err => {
console.log('..................'); // 可以捕獲到
console.log('err:', err);
})
異常捕獲
對于多數(shù)開發(fā)者來說,錯誤處理最自然的形式就是同步的try..catch
結(jié)構(gòu)朴下。遺憾的是努咐,它只能是同步的,無法用于異步代碼模塊殴胧。
- error-firset回調(diào)設計風格錯誤處理:多級error-first回調(diào)交織在一起渗稍,再加上if檢查語句,很容易引發(fā)回調(diào)地獄的風險
- 分離回調(diào)風格錯誤處理:接收兩個參數(shù)团滥,一個回調(diào)用于完成情況竿屹,一個回調(diào)用于拒絕情況(非必填)。
分離回調(diào)風格的錯誤錯誤易于出錯灸姊,如果沒有傳入第二個拒絕回調(diào)拱燃,非常容易造成錯誤被吞掉。
為了避免丟失被忽略和拋棄的Promise錯誤力惯,一些開發(fā)者表示Promise鏈的一個最佳實踐就是最后總以一個catch(...)結(jié)束
碗誉,比如:
var p = Promise.resolve(42);
p.then(function fulfilled(msg){
console.log(msg.toLowerCase); // 數(shù)字沒有string函數(shù),會拋錯
})
.catch( handleErrors )
因為我們沒有為then(..)傳入拒絕處理函數(shù)父晶,所以默認的處理函數(shù)被替換掉了哮缺,而這僅僅是把錯誤傳遞給了鏈中的下一個Promise。因此甲喝,進入p的錯誤以及p之后進入其決議的錯誤都會傳遞到最后的handleErrors(...)
??? Q:如果handleErrors本身內(nèi)部也有錯誤怎么辦呢蝴蜓?
?? A:瀏覽器有一個特定的功能是我們的代碼所沒有的,它們可以跟蹤并了解所有對象被丟棄以及垃圾回收的機制俺猿。所以茎匠,瀏覽器可以追蹤Promise對象。如果在它被垃圾回收的時候其中有拒絕押袍,瀏覽器就能夠確保這是一個真正的未捕獲錯誤诵冒,進而可以確定應該將其報告到開發(fā)者終端。
Promise的使用
- 例1:
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
// 1 2 4 3
- 例2
const promise = new Promise((resolve, reject) => {
resolve('第 1 次 resolve')
console.log('resolve后的普通邏輯')
reject('error')
resolve('第 2 次 resolve')
})
promise
.then((res) => {
console.log('then: ', res)
})
.catch((err) => {
console.log('catch: ', err)
})
// resolve后的普通邏輯
// then: 第 1 次 resolve
Promise 對象的狀態(tài)只能被改變一次谊惭。 我們忽略的是第一次 resolve 后的 reject汽馋、resolve侮东,而不是忽略它身后的所有代碼。因此 console.log(‘resolve后的普通邏輯’) 這句豹芯,仍然可以正常被執(zhí)行悄雅。
- 例3 值穿透問題
Promise.resolve(1)
.then(Promise.resolve(2))
.then(3)
.then()
.then(console.log)
// 1
then 方法里允許我們傳入兩個參數(shù):onFulfilled(成功態(tài)的處理函數(shù))和 onRejected(失敗態(tài)的處理函數(shù))。
可以兩者都傳铁蹈,也可以只傳前者或者后者宽闲。但是無論如何,then 方法的入?yún)⒅荒苁呛瘮?shù)握牧,其他都會被忽略容诬。
在這個過程中,我們最初 resolve 出來那個值沿腰,穿越了一個又一個無效的 then 調(diào)用览徒,就好像是這些 then 調(diào)用都是透明的、不存在的一樣颂龙,因此這種情形我們也形象地稱它是 Promise 的“值穿透”习蓬。
手寫一個Promise的 polyfill
精簡版
function CutePromise(executor){
this.value = null; //記錄異步任務成功的執(zhí)行結(jié)果
this.reason = null; //記錄異步任務失敗的原因
this.status = 'pending'; //記錄當前的狀態(tài) 初始化為pending
//緩存兩個隊列,維護resolved和rejected各自對應的處理函數(shù)
this.onResolvedQueue = [];
this.onRejectedQueue = [];
var self = this;
function resolve(value){
if(self.status !== 'pending'){
return;
}
self.value = value;
self.status = 'resolved';
//用setTimeout延遲隊列任務的執(zhí)行
setTimeout(function(){
self.onResolvedQueue.forEach(resolved => resolved(self.value));
})
}
function reject(reason){
if(self.status !== 'pending'){
return;
}
self.reason = reason;
self.status = 'rejected';
setTimeout(function(){
self.onRejectedQueue.forEach(rejected => rejected(self.reason));
})
}
//把 resolve 和 reject 能力賦予執(zhí)行器
executor(resolve, reject);
}
CutePromise.prototype.then = function(onResolved, onRejected){
if(typeof onResolved !== 'function'){
onResolved = function(x){ return x };
}
if(typeof onRejected !== 'function'){
onRejected = function(e){ throw e };
}
var self = this;
if(self.status === 'resolved'){
onResolved(self.value);
}else if(self.status === 'rejected'){
onRejected(self.reason);
}else if(self.status === 'pending'){
//如果是pending狀態(tài)措嵌,則只對任務做入隊列處理
self.onResolvedQueue.push(onResolved);
self.onRejectedQueue.push(onRejected);
}
return this; //鏈式調(diào)用 ?? 真實的場景是返回一個新的Promise實例
}
new CutePromise(function(resolve, reject){
resolve('成了躲叼!');
}).then((value) => {
console.log(value)
console.log('我是第 1 個任務')
return '第一個任務的結(jié)果'
}).then(value => {
console.log(value);
console.log('我是第 2 個任務')
});
// 依次輸出“成了!” “我是第 1 個任務” “我是第 2 個任務