前言
Promise對(duì)于前端的重要性自不必多說(shuō),網(wǎng)上文章也很多,那我為什么還要重復(fù)寫這篇呢篡石?因?yàn)槟呐驴偨Y(jié)的不準(zhǔn)確不全面,原理這東西還是得自己總結(jié)調(diào)試西采,細(xì)節(jié)太多了凰萨,本篇只是簡(jiǎn)單介紹下規(guī)范,并不會(huì)全盤照搬械馆,重點(diǎn)還是實(shí)現(xiàn)的準(zhǔn)確性胖眷,供大家參考。
本篇介紹
- 介紹術(shù)語(yǔ)和規(guī)范霹崎,這東西看似不重要珊搀,但很容易混淆,影響記憶質(zhì)量
- 通過(guò)PromiseA+規(guī)范自己封裝一個(gè)Promise類
- Promise API 的使用和原理
- Promise常見(jiàn)的問(wèn)題
一尾菇、術(shù)語(yǔ)和規(guī)范
術(shù)語(yǔ)
- thenable:如果一個(gè)對(duì)象或函數(shù)有一個(gè)方法名稱是then境析,那么就說(shuō)它是“具有調(diào)用then方法能力的”囚枪,able在英語(yǔ)語(yǔ)境里是具有某能力的意思。
- promise:thenable的對(duì)象或函數(shù)劳淆,是Promise的實(shí)例链沼,遵循PromiseA+規(guī)范;規(guī)范可以理解為產(chǎn)品說(shuō)明書沛鸵,promise是產(chǎn)品括勺,Promise類是生產(chǎn)產(chǎn)品的工廠
- value:promise成功解決時(shí),傳入resolve回調(diào)函數(shù)中的參數(shù)曲掰,規(guī)范中寫明了各種可能的數(shù)據(jù)類型疾捍,如 undefined、thenable 或一個(gè)新的 promise 等
- reason:promise失敗時(shí)蜈缤,傳入reject回調(diào)函數(shù)的參數(shù)拾氓,表明拒絕的原因
規(guī)范
Promise States
Promise 應(yīng)該有三種狀態(tài),通過(guò)調(diào)用 resolve/reject 方法來(lái)改變狀態(tài)底哥,一經(jīng)改變后不可修改咙鞍。
狀態(tài) | 描述 |
---|---|
pending | - 初始默認(rèn)狀態(tài),表示期約正在等待解決或拒絕<br />- 調(diào)用 resolve() 會(huì)將其變?yōu)?fulfilled 狀態(tài)<br />- 調(diào)用 reject() 會(huì)將其變?yōu)?rejected 狀態(tài) |
fulfilled | 期約解決的狀態(tài)趾徽,為最終態(tài)续滋,后續(xù)操作狀態(tài)均不可改變 |
rejected | 期約拒絕的狀態(tài),為最終態(tài) |
狀態(tài)流轉(zhuǎn)過(guò)程:
then
promise 應(yīng)該有一個(gè)then方法孵奶,當(dāng)解決或拒絕時(shí)會(huì)調(diào)用 then 方法來(lái)處理結(jié)果 x疲酌,并返回一個(gè)promise,其狀態(tài)依賴處理結(jié)果 x了袁。
promise.then(onFulfilled, onRejected)
-
參數(shù)
- onFulfilled 和 onRejected 必須為函數(shù)朗恳,否則會(huì)被忽略
-
onFulfilled
- promise 狀態(tài)變?yōu)?fulfilled 時(shí),要調(diào)用then中的 onFulfilled() 方法载绿,傳入?yún)?shù) value
-
onRejected
- promise 狀態(tài)變?yōu)?rejected 時(shí)粥诫,調(diào)用 onRejected() 方法,傳入?yún)?shù) reason
-
onFulfilled 和 onRejected 共性
- 狀態(tài)為 pending 時(shí)不可調(diào)用崭庸;
- 只允許調(diào)用一次怀浆;
- 應(yīng)該是個(gè)微任務(wù)(通過(guò) queueMicrotask 包裝傳入的回調(diào)實(shí)現(xiàn));
-
then() 方法可被多次調(diào)用
- then()方法執(zhí)行時(shí)怕享,會(huì)把回調(diào)添加到隊(duì)列中执赡,當(dāng)狀態(tài)從 pending 變?yōu)榻鉀Q/拒絕時(shí),會(huì)依次執(zhí)行這些回調(diào)
-
then() 的返回值是個(gè) promise
promise2 = promise1.then(onFulfilled, onRejected)
- 調(diào)用 then 時(shí)函筋,promise2 就已經(jīng)創(chuàng)建沙合,接下來(lái)有兩種情況改變其狀態(tài):
- 當(dāng) onFulfilled 或 onRejected 正常傳入,并執(zhí)行返回結(jié)果 x 后跌帐,調(diào)用一個(gè)方法名為 resolvePromise 的處理函數(shù)首懈,將結(jié)果 x 傳參進(jìn)去芳来,promise2 就會(huì)根據(jù)結(jié)果解決或拒絕;
- onFulfilled 或 onRejected 未傳入猜拾,則 promise2 根據(jù) promise1 的 value/reason 觸發(fā)狀態(tài)變更 fulfilled/rejected
-
resolvePromise
resolvePromise(promise2, x, resolve, reject)
-
情況一:promise2 和 x 是同一引用
傳入 promise2 是為了判斷 x 是否就是 promise2,出現(xiàn)原因是 promise2 是 then 執(zhí)行后立刻返回的佣盒,所以 then 中的回調(diào)函數(shù)是能訪問(wèn)到作用域鏈上端的該變量的挎袜,這種自己的狀態(tài)等待自己狀態(tài)變更才能變更的錯(cuò)誤邏輯,會(huì)直接調(diào)用 reject(reason) 將 promise2 變?yōu)榫芙^肥惭,reason 是 TypeError
-
情況二:x 是一個(gè)新的promise
此時(shí) promise2 取 x 的最終狀態(tài)盯仪,因?yàn)閜romise可能還會(huì)得到promise,而promise2會(huì)在最后一個(gè)非promise處解決或拒絕
-
情況三:x 是一個(gè)對(duì)象或函數(shù)
首先判斷是否有 then 方法蜜葱,沒(méi)有直接拒絕全景,否則將其視為一個(gè)未執(zhí)行 then 的 promise,在 x 環(huán)境中執(zhí)行一下 then牵囤,由于其是用戶自己實(shí)現(xiàn)的 then 方法爸黄,onFulfilled 中對(duì)結(jié)果 y 調(diào)用 resolvePromise,用以解決或拒絕 promise2揭鳞;根據(jù) promise 規(guī)范中的 then 方法對(duì)用戶的 then 方法做判斷并處理異常炕贵。
二、實(shí)現(xiàn) Promise
這里為了看著更符合直覺(jué)野崇,直接用 ES6 的類來(lái)實(shí)現(xiàn)称开,調(diào)用方法形如 new MyPromise(...)
。
1. 先看著規(guī)范把實(shí)例結(jié)構(gòu)搭出來(lái)
// 定義狀態(tài)
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
// MyPromise類
class MyPromise {
// 狀態(tài)變更回調(diào)函數(shù)隊(duì)列
FULFILLED_CALLBACK_LIST = [];
REJECTED_CALLBACK_LIST = [];
constructor(fn) {
// 實(shí)例屬性1:狀態(tài)
this.status = PENDING; // 初始化是pending狀態(tài)
// 實(shí)例屬性2:結(jié)果/原因
this.value = null;
this.reason = null;
}
resolve(value) {
}
reject(reason) {
}
then(onFulfilled, onRejected) {
}
resolvePromise(promise2, x, resolve, reject) {
}
}
2. 實(shí)現(xiàn) resolve 和 reject
這兩個(gè) api 調(diào)用時(shí)就是為了改變狀態(tài)乓梨,狀態(tài)變更后的邏輯放到 set status() {}
中實(shí)現(xiàn)鳖轰,這樣做的話 api 的工作更專一。
class MyPromise {
constructor(fn) {
// ...
// 創(chuàng)建實(shí)例時(shí)就會(huì)調(diào)用傳入的 fn 函數(shù)
try {
fn(
this.resolve.bind(this),
this.reject.bind(this)
);
} catch(err) {
this.reject(err); // 非函數(shù)就拒絕
}
}
resolve(value) {
if(this.status === PENDING) {
this.status = FULFILLED; // 變更狀態(tài)
this.value = value; // 保存值供后續(xù)邏輯使用
}
}
reject(reason) {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
}
}
}
3. 實(shí)現(xiàn) then 方法
- 對(duì)回調(diào)進(jìn)行兼容扶镀,透?jìng)?value/reason蕴侣;
- then 方法返回 promise,根據(jù)狀態(tài)決定如果回調(diào)處理邏輯狈惫;
- 回調(diào)要求是微任務(wù)睛蛛,所以要對(duì)其封裝一層;
function isFunction(param) {
return typeof param === 'function';
}
class MyPromise {
then(onFulfilled, onRejected) {
/************* 1. 兼容回調(diào) *************/
// 若未傳入回調(diào)胧谈,則 promise2 的狀態(tài)和value/reason 都與 promise1 一致
// 所以平時(shí)寫的 catch 方法其實(shí)是 promise2 調(diào)用的忆肾,會(huì)將結(jié)果透?jìng)鬟M(jìn)去
const realOnFulfilled = isFunction(onFulfilled) ? onFulfilled : (value) => {
return value;
};
const realOnRejected = isFunction(onRejected) ? onRejected : reason => {
throw reason;
};
/************* 2. 返回值是promise *************/
const promise2 = new MyPromise((resolve, reject) => {
/************* 3. 封裝微任務(wù),并用resolvePromise處理回調(diào)結(jié)果 *************/
const fulfilledMicrotask = () => {
queueMicrotask(() => {
try {
const x = realOnFulfilled(this.value);
// 根據(jù)then回調(diào)結(jié)果處理promise2
this.resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err); // 若執(zhí)行過(guò)程中報(bào)錯(cuò)菱肖,則直接拒絕
}
});
}
const rejectedMicrotask = () => {
queueMicrotask(() => {
try {
const x = realOnRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch(err) {
reject(err);
}
});
}
/************* 4. 根據(jù)當(dāng)前實(shí)例狀態(tài)決定調(diào)用then的哪個(gè)回調(diào) *************/
switch(this.status) {
case FULFILLED:
fulfilledMicrotask(); // 若狀態(tài)已為最終態(tài)客冈,則直接執(zhí)行回調(diào)
break;
case REJECTED:
rejectedMicrotask();
break;
case PENDING:
// 若狀態(tài)是pending,則先緩存回調(diào)
// 在pending狀態(tài)變更之前稳强,then可以被多次調(diào)用场仲,所以要用隊(duì)列來(lái)維護(hù)回調(diào)
this.FULFILLED_CALLBACK_LIST.push(fulfilledMicrotask);
this.REJECTED_CALLBACK_LIST.push(rejectedMicrotask);
}
});
/************* 5. 返回promise *************/
return promise2;
}
}
4. 狀態(tài)變更邏輯
當(dāng)狀態(tài)改變時(shí)和悦,要清空?qǐng)?zhí)行回調(diào)列表,這里用setter監(jiān)聽(tīng)變更渠缕,所以需要將實(shí)例屬性status進(jìn)行改造:
class MyPromise {
constructor(fn) {
// ...
this._status = PENDING; // 原始變量
}
get status() { // getter
return this._status;
}
set status(newStatus) { // setter
this._status = newStatus;
switch(newStatus) {
case FULFILLED:
this.FULFILLED_CALLBACK_LIST.forEach(callback => {
callback(this.value);
});
break;
case REJECTED:
this.REJECTED_CALLBACK_LIST.forEach(callback => {
callback(this.reason);
});
break;
}
}
}
5. 實(shí)現(xiàn) resolvePromise
這個(gè)函數(shù)是對(duì) then 中回調(diào)結(jié)果 x 分情況討論鸽素,不同情況會(huì)解決或拒絕 then 返回的 promise2;情況比較多亦鳞,所以需要多加練習(xí)并記憶:
resolvePromise(promise2, x, resolve, reject) {
/*********** 情況1:是自己 ***********/
if(promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
/*********** 情況2:是promise ***********/
if(x instanceof MyPromise) {
x.then((y) => {
// 遞歸下去馍忽,直到遇到第一個(gè)非promise,promise2就會(huì)解決/拒絕
this.resolvePromise(promise2, y, resolve, reject);
}, reject);
} else if (typeof x === 'object' && x !== null || isFunction(x)) {
/*********** 情況3:引用類型燕差,判斷是否為thenable ***********/
// 獲取結(jié)果上的then方法
let then = null;
try {
then = x.then;
} catch(err) {
return reject(err); // 防止用戶寫個(gè)會(huì)拋錯(cuò)的getter
}
// 判斷是否為thenable
if(isFunction(then)) {
let called = false;
// 由于是thenable遭笋,就當(dāng) x是其他符合規(guī)范的 Promise的實(shí)例
// 所以then要在實(shí)例環(huán)境進(jìn)行才能正確拿到this.value等
try {
then.call(
x,
y => {
if(called) return; // 方法不能重復(fù)調(diào)用
called = true;
this.resolvePromise(promise2, y, resolve, reject);
},
r => {
if(called) return;
called = true;
reject(r);
}
);
} catch(err) {
// 防止then中調(diào)用完onFulfilled(value)后拋個(gè)錯(cuò)之類的情況
if(called) return;
reject(err);
}
} else {
resolve(x); // 普通引用類型直接解決
}
} else {
/*********** 情況4:基礎(chǔ)類型 ***********/
resolve(x); // 基本類型直接解決
}
}
6. 補(bǔ)充上實(shí)例方法 catch
上述5點(diǎn),其實(shí)已經(jīng)能跑如下測(cè)試了:
const test = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('解決');
}, 1000);
}).then(console.log);
console.log('同步代碼');
setTimeout(() => {
console.log('宏任務(wù)');
}, 2000);
結(jié)果如圖:
不過(guò)徒探,完整的 promise 實(shí)例還包括 catch瓦呼、finally 方法,catch() 其實(shí)就是 then() 方法僅傳入第二個(gè)錯(cuò)誤處理回調(diào)的包裝函數(shù)测暗,目的是更加重點(diǎn)關(guān)注異步調(diào)用的錯(cuò)誤而非結(jié)果央串;
而 finally() 方法是不管狀態(tài)如何都執(zhí)行回調(diào),需要注意的是偷溺,finally 僅表示完成蹋辅,但狀態(tài)未知,也就不能給用戶提供value 或 reason挫掏,因?yàn)闆](méi)法做區(qū)分侦另,所以不能給回調(diào)帶參數(shù);而且 finally 還有另一個(gè)特性尉共,就是當(dāng)回調(diào)未報(bào)錯(cuò)或者不是一個(gè) rejected 狀態(tài)的 promise 時(shí)褒傅,finally 的返回值要求是個(gè)能透?jìng)髟?promise 結(jié)果的 promise,具體可見(jiàn)代碼注釋:
catch(onRejected) {
return this.then(null, onRejected);
}
finally(cb) {
// 無(wú)論狀態(tài)如何袄友,都執(zhí)行回調(diào)殿托,且返回值是個(gè) promise,那么自然想到用 then(cb, cb)
// 但 finally 有兩個(gè)要求剧蚣,1. 回調(diào)不能帶參數(shù)支竹,2. 透?jìng)髟璸romise結(jié)果
// 所以不難想到封裝一層函數(shù)
// 但需要注意的是,若 cb 返回個(gè)promise鸠按,則需等待promise狀態(tài)解決才能改變?yōu)橥競(jìng)鹘Y(jié)果
// 所以這里用Promise.resolve()包一層兼容 cb是 promise的情況
return this.then(
value => {
// cb()解決會(huì)透?jìng)鲾?shù)據(jù)礼搁,拒絕會(huì)走常規(guī)流程,即暴露 cb自己的 reason
return MyPromise.resolve(cb()).then(() => value);
},
reason => {
// cb()解決才會(huì)透?jìng)髟?promise的 reason目尖,供后續(xù) catch使用
// 拒絕會(huì)走常規(guī)流程馒吴,即暴露 cb自己的 reason
return MyPromise.resolve(cb()).then(() => { throw reason });
});
}
7. 補(bǔ)充上類靜態(tài)方法 resolve、reject、race饮戳、all
除了實(shí)例用法豪治,Promise 類本身有幾個(gè)常見(jiàn)靜態(tài)方法:
-
Promise.all(list: iterable)
:all 方法傳入可迭代結(jié)構(gòu)如數(shù)組,每項(xiàng)可以是任意類型或promise扯罐,內(nèi)部會(huì)將所有項(xiàng)轉(zhuǎn)化為期約负拟,返回值是個(gè) promise,當(dāng)所有結(jié)果都正確返回后才會(huì)解決歹河,有任意一個(gè)期約項(xiàng)為 reject 則返回值的 promise 就是拒絕齿椅;若要所有結(jié)果,哪怕是某項(xiàng)狀態(tài)為 rejected启泣,那就用Promise.allSettled()
-
Promise.race(list: iterable)
:傳參同 all 方法,返回值也是 promise示辈,區(qū)別是當(dāng)某項(xiàng)期約解決或拒絕后寥茫,結(jié)果就直接解決或拒絕,其結(jié)果就是這個(gè)最先完成的期約value或reason -
Promise.resolve(promise | thenable | any)
:返回一個(gè)promise矾麻,狀態(tài)視傳入值而定纱耻,若傳入的是 promise則冪等返回原promise,若為thenable险耀,則執(zhí)行 then 方法弄喘,promise 狀態(tài)跟隨 then 的結(jié)果;若是其他類型值甩牺,則返回的 promise 的狀態(tài)直接為 fulfilled蘑志,value值就是傳入的數(shù)據(jù) -
Promise.reject(promise | thenable | any)
:返回一個(gè)狀態(tài)為 rejected 的 promise,reason值就是傳入的參數(shù)
還有 Promise.allSettled()
贬派,Promise.any()
這兩個(gè)方法和 Promise.all() 類似急但,且面試題也會(huì)出一些變種,比如任務(wù)有優(yōu)先級(jí)的概念等搞乏,這個(gè)等之后總結(jié)面試題專題時(shí)再寫波桩,因?yàn)閱?wèn)原理時(shí)一般只會(huì)問(wèn)到 then() 方法,所以這里先簡(jiǎn)單實(shí)現(xiàn) Promise.all() 和 Promise.race()请敦,另外2個(gè) api 以及變種面試題之后再討論镐躲。
/************* Promise.resolve(value) *************/
static resolve(value) {
// 若已經(jīng)是promise,則冪等返回
if (value instanceof MyPromise) {
return value;
}
// 否則返回一個(gè)promise侍筛,狀態(tài)依賴value
return new MyPromise((resolve) => {
resolve(value);
});
}
/************* Promise.reject(reason) *************/
static reject(reason) {
// 返回一個(gè)拒絕的promise萤皂,注意是個(gè)新的 promise
return new MyPromise((resolve, reject) => {
reject(reason);
});
}
/************* Promise.race(list) *************/
static race(anyList) {
return new MyPromise((resolve, reject) => {
const len = anyList.length;
if(len === 0) {
resolve(); // 無(wú)數(shù)據(jù)時(shí)直接返回一個(gè)空promise
} else {
for(let i = 0; i < len; i++) {
MyPromise.resolve(anyList).then(
value => {
resolve(value); // 只要有某項(xiàng)解決就將結(jié)果解決
},
reason => {
reject(reason); // 只要有某項(xiàng)拒絕就將結(jié)果拒絕
}
);
}
}
})
}
/************* Promise.all(list) *************/
static all(anyList) { // 1. all是靜態(tài)方法
// 2. 返回值是promise
return new MyPromise((resolve, reject) => {
// 3. 參數(shù)類型判斷,需要傳入可迭代結(jié)構(gòu)
if(!anyList || typeof anyList[Symbol.iterator] !== 'function') {
return reject(new TypeError('arguments must be iterable'));
}
const len = anyList.length;
const res = [];
let counter = 0;
for(let i = 0; i < len; i++) {
// 4. 參數(shù)類型期約化
MyPromise.resolve(anyList[i]).then(value => {
counter++;
// 5. 不能用push勾笆,因?yàn)榻Y(jié)果順序與參數(shù)一一對(duì)應(yīng)
res[i] = value;
// 等待所有結(jié)果成功返回后解決期約
if(counter === len) {
resolve(res);
}
}).catch(reason => {
reject(reason);
});
}
});
}
跑一段測(cè)試代碼:
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
})
const p2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 2000);
})
const p3 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
})
Promise.all([p2, p1, p3]).then(res => {
console.log('all_成功 ', res);
}).catch(e => {
console.log('all_失敗 ', e);
});
Promise.race([p2, p1, p3]).then(res => {
console.log('race_成功 ', res);
}).catch(e => {
console.log('race_失敗 ', e);
});
拒絕期約的測(cè)試代碼可以自己改動(dòng)敌蚜,不再贅述。
三窝爪、總結(jié)
至此已初步根據(jù)規(guī)范實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的Promise弛车,細(xì)節(jié)并沒(méi)有考究很細(xì)齐媒,比如參數(shù)類型的校驗(yàn),兼容性的考究纷跛,以及全部靜態(tài)方法的實(shí)現(xiàn)等等喻括;因?yàn)槲蚁雮鬟_(dá)的是,Promise原理為何每篇文章實(shí)現(xiàn)都不一樣贫奠,為啥一定要有 then 方法唬血,或?yàn)樯队心敲炊?try-catch,這一切讓人難以理解或記憶的原因唤崭,就是有一個(gè)東西叫PromiseA+規(guī)范拷恨,規(guī)范就像試卷上的題目,要求是啥樣谢肾,就得實(shí)現(xiàn)成啥樣腕侄;理解了這個(gè)大前提,代碼實(shí)現(xiàn)方式是否嚴(yán)謹(jǐn)優(yōu)雅芦疏,就完全看你自己和面試官要求了冕杠。剩下的 allSettled() 和 any() 方法,以及并發(fā)請(qǐng)求的變種面試題酸茴,會(huì)在之后總結(jié)分预,因?yàn)榇笾滤悸范枷嗨疲?Promise 原理考察也不太會(huì)關(guān)心這幾個(gè)類似的api薪捍,因此將這一類整理到一起再總結(jié)笼痹。
我自己用 node 17.3.1 版本跑通了所有測(cè)試,可能實(shí)現(xiàn)的地方都疏漏之處酪穿,望大家?guī)兔χ刚氤粍俑屑ぁV翱催^(guò)很多文章昆稿,發(fā)現(xiàn)我不理解的地方纺座,別人都會(huì)一嘴帶過(guò),有的博客甚至就是復(fù)制粘貼溉潭,沒(méi)經(jīng)過(guò)自己的思考净响,可想而知我看到這些文章時(shí)腦袋是有多大。話雖如此喳瓣,我自己總結(jié)的這篇文章也會(huì)有讓人不理解的地方馋贤,不過(guò)準(zhǔn)確性還是能保證的,有不理解的地方可以給我評(píng)論留言畏陕,我會(huì)一一解答的配乓,源碼放到了下面的參考鏈接中。