模擬實(shí)現(xiàn) Promise(小白版)
本篇來(lái)講講如何模擬實(shí)現(xiàn)一個(gè) Promise 的基本功能尊勿,網(wǎng)上這類(lèi)文章已經(jīng)很多,本篇筆墨會(huì)比較多畜侦,因?yàn)橄胗米约旱睦斫庠樱冒自?huà)文來(lái)講講
Promise 的基本規(guī)范,參考了這篇:【翻譯】Promises/A+規(guī)范
但說(shuō)實(shí)話(huà)旋膳,太多的專(zhuān)業(yè)術(shù)語(yǔ)澎语,以及基本按照標(biāo)準(zhǔn)規(guī)范格式翻譯而來(lái),有些內(nèi)容验懊,如果不是對(duì)規(guī)范的閱讀方式比較熟悉的話(huà)擅羞,那是很難理解這句話(huà)的內(nèi)容的
我就是屬于沒(méi)直接閱讀過(guò)官方規(guī)范的,所以即使在看中文譯版時(shí)义图,有些表達(dá)仍舊需要花費(fèi)很多時(shí)間去理解减俏,基于此,才想要寫(xiě)這篇
Promise 基本介紹
Promise 是一種異步編程方案碱工,通過(guò) then 方法來(lái)注冊(cè)回調(diào)函數(shù)娃承,通過(guò)構(gòu)造函數(shù)參數(shù)來(lái)控制異步狀態(tài)
Promise 的狀態(tài)變化有兩種奏夫,成功或失敗,狀態(tài)一旦變更結(jié)束历筝,就不會(huì)再改變酗昼,后續(xù)所有注冊(cè)的回調(diào)都能接收此狀態(tài),同時(shí)異步執(zhí)行結(jié)果會(huì)通過(guò)參數(shù)傳遞給回調(diào)函數(shù)
使用示例
var p = new Promise((resolve, reject) => {
// do something async job
// resolve(data); // 任務(wù)結(jié)束梳猪,觸發(fā)狀態(tài)變化仔雷,通知成功回調(diào)的處理,并傳遞結(jié)果數(shù)據(jù)
// reject(err); // 任務(wù)異常舔示,觸發(fā)狀態(tài)變化,通知失敗回調(diào)的處理电抚,并傳遞失敗原因
}).then(value => console.log(value))
.catch(err => console.error(err));
p.then(v => console.log(v), err => console.error(err));
上述例子是基本用法惕稻,then 方法返回一個(gè)新的 Promise,所以支持鏈?zhǔn)秸{(diào)用蝙叛,可用于一個(gè)任務(wù)依賴(lài)于上一個(gè)任務(wù)的執(zhí)行結(jié)果這種場(chǎng)景
對(duì)于同一個(gè) Promise 也可以調(diào)用多次 then 來(lái)注冊(cè)多個(gè)回調(diào)處理
通過(guò)使用來(lái)理解它的功能俺祠,清楚它都支持哪些功能后,我們?cè)谀M實(shí)現(xiàn)時(shí)借帘,才能知道到底需要寫(xiě)些什么代碼
所以蜘渣,這里來(lái)比較細(xì)節(jié)的羅列下 Promise 的基本功能:
- Promise 有三種狀態(tài):Pending(執(zhí)行中)、Resolved(成功)肺然、Rejected(失斈韪住),狀態(tài)一旦變更結(jié)束就不再改變
- Promise 構(gòu)造函數(shù)接收一個(gè)函數(shù)參數(shù)际起,可以把它叫做 task 處理函數(shù)
- task 處理函數(shù)用來(lái)處理異步工作拾碌,這個(gè)函數(shù)有兩個(gè)參數(shù),也都是函數(shù)類(lèi)型街望,當(dāng)異步工作結(jié)束校翔,就是通過(guò)調(diào)用這兩個(gè)函數(shù)參數(shù)來(lái)通知 Promise 狀態(tài)變更、回調(diào)觸發(fā)灾前、結(jié)果傳遞
- Promise 有一個(gè) then 方法用于注冊(cè)回調(diào)處理防症,當(dāng)狀態(tài)變化結(jié)束,注冊(cè)的回調(diào)一定會(huì)被處理哎甲,即使是在狀態(tài)變化結(jié)束后才通過(guò) then 注冊(cè)
- then 方法支持調(diào)用多次來(lái)注冊(cè)多個(gè)回調(diào)處理
- then 方法接收兩個(gè)可選參數(shù)蔫敲,這兩個(gè)參數(shù)類(lèi)型都是函數(shù),也就是需要注冊(cè)的回調(diào)處理函數(shù)炭玫,分別是成功時(shí)的回調(diào)函數(shù)燕偶,失敗時(shí)的回調(diào)函數(shù)
- 這些回調(diào)函數(shù)有一個(gè)參數(shù),類(lèi)型任意础嫡,值就是任務(wù)結(jié)束需要通知給回調(diào)的結(jié)果指么,通過(guò)調(diào)用 task 處理函數(shù)的參數(shù)(類(lèi)型是函數(shù))傳遞過(guò)來(lái)
- then 方法返回一個(gè)新的 Promise酝惧,以便支持鏈?zhǔn)秸{(diào)用,新 Promise 狀態(tài)的變化依賴(lài)于回調(diào)函數(shù)的返回值伯诬,不同類(lèi)型處理方式不同
- then 方法的鏈?zhǔn)秸{(diào)用中晚唇,如果中間某個(gè) then 傳入的回調(diào)處理不能友好的處理回調(diào)工作(比如傳遞給 then 非函數(shù)類(lèi)型參數(shù)),那么這個(gè)工作會(huì)繼續(xù)往下傳遞給下個(gè) then 注冊(cè)的回調(diào)函數(shù)
- Promise 有一個(gè) catch 方法盗似,用于注冊(cè)失敗的回調(diào)處理哩陕,其實(shí)是
then(null, onRejected)
的語(yǔ)法糖 - task 處理函數(shù)或者回調(diào)函數(shù)執(zhí)行過(guò)程發(fā)生代碼異常時(shí),Promise 內(nèi)部自動(dòng)捕獲赫舒,狀態(tài)直接當(dāng)做失敗來(lái)處理
-
new Promise(task)
時(shí)悍及,傳入的 task 函數(shù)就會(huì)馬上被執(zhí)行了,但傳給 then 的回調(diào)函數(shù)接癌,會(huì)作為微任務(wù)放入隊(duì)列中等待執(zhí)行(通俗理解心赶,就是降低優(yōu)先級(jí),延遲執(zhí)行缺猛,不知道怎么模擬微任務(wù)的話(huà)缨叫,可以使用 setTimeout 生成的宏任務(wù)來(lái)模擬)
這些基本功能就足夠 Promise 的日常使用了,所以我們的模擬實(shí)現(xiàn)版的目標(biāo)就是實(shí)現(xiàn)這些功能
模擬實(shí)現(xiàn)思路
第一步:骨架
Promise 的基本功能清楚了荔燎,那我們代碼該怎么寫(xiě)耻姥,寫(xiě)什么?
從代碼角度來(lái)看的話(huà)有咨,無(wú)非也就是一些變量琐簇、函數(shù),所以座享,我們就可以來(lái)針對(duì)各個(gè)功能點(diǎn)鸽嫂,思考下,都需要哪些代碼:
- 變量上至少需要:三種狀態(tài)征讲、當(dāng)前狀態(tài)(_status)据某、傳遞給回調(diào)函數(shù)的結(jié)果值(_value)
- 構(gòu)造函數(shù) constructor
task 處理函數(shù)- task 處理函數(shù)的兩個(gè)用于通知狀態(tài)變更的函數(shù)(handleResolve, handleReject)
- then 方法
- then 方法
注冊(cè)的兩個(gè)回調(diào)函數(shù) - 回調(diào)函數(shù)隊(duì)列
- catch 方法
task 處理函數(shù)和注冊(cè)的回調(diào)處理函數(shù)都是使用者在使用 Promise 時(shí),自行根據(jù)業(yè)務(wù)需要編寫(xiě)的代碼
那么诗箍,剩下的也就是我們?cè)趯?shí)現(xiàn) Promise 時(shí)需要編寫(xiě)的代碼了癣籽,這樣一來(lái),Promise 的骨架其實(shí)也就可以出來(lái)了:
export type statusChangeFn = (value?: any) => void;
/* 回調(diào)函數(shù)類(lèi)型 */
export type callbackFn = (value?: any) => any;
export class Promise {
/* 三種狀態(tài) */
private readonly PENDING: string = 'pending';
private readonly RESOLVED: string = 'resolved';
private readonly REJECTED: string = 'rejected';
/* promise當(dāng)前狀態(tài) */
private _status: string;
/* promise執(zhí)行結(jié)果 */
private _value: string;
/* 成功的回調(diào) */
private _resolvedCallback: Function[] = [];
/* 失敗的回調(diào) */
private _rejectedCallback: Function[] = [];
/**
* 處理 resolve 的狀態(tài)變更相關(guān)工作滤祖,參數(shù)接收外部傳入的執(zhí)行結(jié)果
*/
private _handleResolve(value?: any) {}
/**
* 處理 reject 的狀態(tài)變更相關(guān)工作筷狼,參數(shù)接收外部傳入的失敗原因
*/
private _handleReject(value?: any) {}
/**
* 構(gòu)造函數(shù),接收一個(gè) task 處理函數(shù)匠童,task 有兩個(gè)可選參數(shù)埂材,類(lèi)型也是函數(shù),其實(shí)也就是上面的兩個(gè)處理狀態(tài)變更工作的函數(shù)(_handleResolve汤求,_handleReject)俏险,用來(lái)給使用者來(lái)觸發(fā)狀態(tài)變更使用
*/
constructor(task: (resolve?: statusChangeFn, reject?: statusChangeFn) => void) {}
/**
* then 方法严拒,接收兩個(gè)可選參數(shù),用于注冊(cè)成功或失敗時(shí)的回調(diào)處理竖独,所以類(lèi)型也是函數(shù)裤唠,函數(shù)有一個(gè)參數(shù),接收 Promise 執(zhí)行結(jié)果或失敗原因莹痢,同時(shí)可返回任意值种蘸,作為新 Promise 的執(zhí)行結(jié)果
*/
then(onResolved?: callbackFn, onRejected?: callbackFn): Promise {
return null;
}
catch(onRejected?: callbackFn): Promise {
return this.then(null, onRejected);
}
}
注意:骨架這里的代碼,我用了 TypeScript竞膳,這是一種強(qiáng)類(lèi)型語(yǔ)言航瞭,可以標(biāo)明各個(gè)變量、參數(shù)類(lèi)型坦辟,便于講述和理解刊侯,看不懂沒(méi)關(guān)系,下面有編譯成 js 版的
所以长窄,我們要補(bǔ)充完成的其實(shí)就是三部分:Promise 構(gòu)造函數(shù)都做了哪些事、狀態(tài)變更需要做什么處理纲菌、then 注冊(cè)回調(diào)函數(shù)時(shí)需要做的處理
第二步:構(gòu)造函數(shù)
Promise 的構(gòu)造函數(shù)做的事挠日,其實(shí)很簡(jiǎn)單,就是馬上執(zhí)行傳入的 task 處理函數(shù)翰舌,并將自己內(nèi)部提供的兩個(gè)狀態(tài)變更處理的函數(shù)傳遞給 task嚣潜,同時(shí)將當(dāng)前 promise 狀態(tài)置為 PENDING(執(zhí)行中)
constructor(task) {
// 1. 將當(dāng)前狀態(tài)置為 PENDING
this._status = this.PENDING;
// 參數(shù)類(lèi)型校驗(yàn)
if (!(task instanceof Function)) {
throw new TypeError(`${task} is not a function`);
}
try {
// 2. 調(diào)用 task 處理函數(shù),并將狀態(tài)變更通知的函數(shù)傳遞過(guò)去椅贱,需要注意 this 的處理
task(this._handleResolve.bind(this), this._handleReject.bind(this));
} catch (e) {
// 3. 如果 task 處理函數(shù)發(fā)生異常懂算,當(dāng)做失敗來(lái)處理
this._handleReject(e);
}
}
第三步:狀態(tài)變更
Promise 狀態(tài)變更的相關(guān)處理是我覺(jué)得實(shí)現(xiàn) Promise 最難的一部分,這里說(shuō)的難并不是說(shuō)代碼有多復(fù)雜庇麦,而是說(shuō)這塊需要理解透计技,或者看懂規(guī)范并不大容易,因?yàn)樾枰紤]一些處理山橄,網(wǎng)上看了些 Promise 實(shí)現(xiàn)的文章垮媒,這部分都存在問(wèn)題
狀態(tài)變更的工作,是由傳給 task 處理函數(shù)的兩個(gè)函數(shù)參數(shù)被調(diào)用時(shí)觸發(fā)進(jìn)行航棱,如:
new Promise((resolve, reject) => {
resolve(1);
});
resolve 或 reject 的調(diào)用睡雇,就會(huì)觸發(fā) Promise 內(nèi)部去處理狀態(tài)變更的相關(guān)工作,還記得構(gòu)造函數(shù)做的事吧饮醇,這里的 resolve 或 reject 其實(shí)就是對(duì)應(yīng)著內(nèi)部的 _handleResolve 和 _handleReject 這兩個(gè)處理狀態(tài)變更工作的函數(shù)
但這里有一點(diǎn)需要注意它抱,是不是 resolve 一調(diào)用,Promise 的狀態(tài)就一定發(fā)生變化了呢朴艰?
答案不是的观蓄,網(wǎng)上看了些這類(lèi)文章混移,他們的處理是 resolve 調(diào)用,狀態(tài)就變化蜘腌,就去處理回調(diào)隊(duì)列了
但實(shí)際上沫屡,這樣是錯(cuò)的
狀態(tài)的變更,其實(shí)依賴(lài)于 resolve 調(diào)用時(shí)撮珠,傳遞過(guò)去的參數(shù)的類(lèi)型谤牡,因?yàn)檫@里可以傳遞任意類(lèi)型的值,可以是基本類(lèi)型秸抚,也可以是 Promise
當(dāng)類(lèi)型不一樣時(shí)土居,對(duì)于狀態(tài)的變更處理是不一樣的,開(kāi)頭那篇規(guī)范里面有詳細(xì)的說(shuō)明娶耍,但要看懂并不大容易免姿,我這里就簡(jiǎn)單用我的理解來(lái)講講:
- resolve(x) 觸發(fā)的 pending => resolved 的處理:
- 當(dāng) x 類(lèi)型是 Promise 對(duì)象時(shí):
- 當(dāng) x 這個(gè) Promise 的狀態(tài)變化結(jié)束時(shí),再以 x 這個(gè) Promise 內(nèi)部狀態(tài)和結(jié)果(_status 和 _value)作為當(dāng)前 Promise 的狀態(tài)和結(jié)果進(jìn)行狀態(tài)變更處理
- 可以簡(jiǎn)單理解成當(dāng)前的 Promise 是依賴(lài)于 x 這個(gè) Promise 的榕酒,即
x.then(this._handleResolve, this._handleReject)
- 當(dāng) x 類(lèi)型是 thenable 對(duì)象(具有 then 方法的對(duì)象)時(shí):
- 把這個(gè) then 方法作為 task 處理函數(shù)來(lái)處理胚膊,這樣就又回到第一步即等待狀態(tài)變更的觸發(fā)
- 可以簡(jiǎn)單理解成
x.then(this._handleResolve, this._handleReject)
- 這里的 x.then 并不是 Promise 的 then 處理,只是簡(jiǎn)單的一個(gè)函數(shù)調(diào)用想鹰,只是剛好函數(shù)名叫做 then
- 其余類(lèi)型時(shí):
- 內(nèi)部狀態(tài)(_status)置為 RESOLVE
- 內(nèi)部結(jié)果(_value)置為 x
- 模擬創(chuàng)建微任務(wù)(setTimeout)處理回調(diào)函數(shù)隊(duì)列
- 當(dāng) x 類(lèi)型是 Promise 對(duì)象時(shí):
- reject(x) 觸發(fā)的 pending => rejected 的處理:
- 不區(qū)分 x 類(lèi)型紊婉,直接走 rejected 的處理
- 內(nèi)部狀態(tài)(_status)置為 REJECTED
- 內(nèi)部結(jié)構(gòu)(_value)置為 x
- 模擬創(chuàng)建微任務(wù)(setTimeout)處理回調(diào)函數(shù)隊(duì)列
- 不區(qū)分 x 類(lèi)型紊婉,直接走 rejected 的處理
所以你可以看到,其實(shí) resolve 即使調(diào)用了辑舷,但內(nèi)部并不一定就會(huì)發(fā)生狀態(tài)變化喻犁,只有當(dāng) resolve 傳遞的參數(shù)類(lèi)型既不是 Promise 對(duì)象類(lèi)型,也不是具有 then 方法的 thenable 對(duì)象時(shí)何缓,狀態(tài)才會(huì)發(fā)生變化
而當(dāng)傳遞的參數(shù)是 Promise 或具有 then 方法的 thenable 對(duì)象時(shí)肢础,差不多又是相當(dāng)于遞歸回到第一步的等待 task 函數(shù)的處理了
想想為什么需要這種處理,或者說(shuō)碌廓,為什么需要這么設(shè)計(jì)传轰?
這是因?yàn)椋嬖谶@樣一種場(chǎng)景:有多個(gè)異步任務(wù)谷婆,這些異步任務(wù)之間是同步關(guān)系路召,一個(gè)任務(wù)的執(zhí)行依賴(lài)于上一個(gè)異步任務(wù)的執(zhí)行結(jié)果,當(dāng)這些異步任務(wù)通過(guò) then 的鏈?zhǔn)秸{(diào)用組合起來(lái)時(shí)波材,then 方法產(chǎn)生的新的 Promise 的狀態(tài)變更是依賴(lài)于回調(diào)函數(shù)的返回值股淡。所以這個(gè)狀態(tài)變更需要支持當(dāng)值類(lèi)型是 Promise 時(shí)的異步等待處理,這條異步任務(wù)鏈才能得到預(yù)期的執(zhí)行效果
當(dāng)你們?nèi)タ匆?guī)范廷区,或看規(guī)范的中文版翻譯唯灵,其實(shí)有關(guān)于這個(gè)的更詳細(xì)處理說(shuō)明,比如開(kāi)頭給的鏈接的那篇文章里有專(zhuān)門(mén)一個(gè)模塊:Promise 的解決過(guò)程隙轻,也表示成 [[Resolve]](promise, x)
就是在講這個(gè)
但我想用自己的理解來(lái)描述埠帕,這樣比較容易理解垢揩,雖然我也只能描述個(gè)大概的工作,更細(xì)節(jié)敛瓷、更全面的處理應(yīng)該要跟著規(guī)范來(lái)叁巨,下面就看看代碼:
/**
* resolve 的狀態(tài)變更處理
*/
_handleResolve(value) {
if (this._status === this.PENDING) {
// 1. 如果 value 是 Promise,那么等待 Promise 狀態(tài)結(jié)果出來(lái)后呐籽,再重新做狀態(tài)變更處理
if (value instanceof Promise) {
try {
// 這里之所以不需要用 bind 來(lái)注意 this 問(wèn)題是因?yàn)槭褂昧思^函數(shù)
// 這里也可以寫(xiě)成 value.then(this._handleResole.bind(this), this._handleReject.bind(this))
value.then(v => {
this._handleResolve(v);
},
err => {
this._handleReject(err);
});
} catch(e) {
this._handleReject(e);
}
} else if (value && value.then instanceof Function) {
// 2. 如果 value 是具有 then 方法的對(duì)象時(shí)锋勺,那么將這個(gè) then 方法當(dāng)做 task 處理函數(shù),把狀態(tài)變更的觸發(fā)工作交由 then 來(lái)處理狡蝶,注意 this 的處理
try {
const then = value.then;
then.call(value, this._handleResolve.bind(this), this._handleReject.bind(this));
} catch(e) {
this._handleReject(e);
}
} else {
// 3. 其他類(lèi)型庶橱,狀態(tài)變更、觸發(fā)成功的回調(diào)
this._status = this.RESOLVED;
this._value = value;
setTimeout(() = {
this._resolvedCallback.forEach(callback => {
callback();
});
});
}
}
}
/**
* reject 的狀態(tài)變更處理
*/
_handleReject(value) {
if (this._status === this.PENDING) {
this._status = this.REJECTED;
this._value = value;
setTimeout(() => {
this._rejectedCallback.forEach(callback => {
callback();
});
});
}
}
第四步:then
then 方法負(fù)責(zé)的職能其實(shí)也很復(fù)雜贪惹,既要返回一個(gè)新的 Promise苏章,這個(gè)新的 Promise 的狀態(tài)和結(jié)果又要依賴(lài)于回調(diào)函數(shù)的返回值,而回調(diào)函數(shù)的執(zhí)行又要看情況是緩存進(jìn)回調(diào)函數(shù)隊(duì)列里奏瞬,還是直接取依賴(lài)的 Promise 的狀態(tài)結(jié)果后枫绅,丟到微任務(wù)隊(duì)列里去執(zhí)行
雖然職能復(fù)雜是復(fù)雜了點(diǎn),但其實(shí)硼端,實(shí)現(xiàn)上并淋,都是依賴(lài)于前面已經(jīng)寫(xiě)好的構(gòu)造函數(shù)和狀態(tài)變更函數(shù),所以只要前面幾個(gè)步驟實(shí)現(xiàn)上沒(méi)問(wèn)題显蝌,then 方法也就不會(huì)有太大的問(wèn)題预伺,直接看代碼:
/**
* then 方法订咸,接收兩個(gè)可選參數(shù)曼尊,用于注冊(cè)回調(diào)處理,所以類(lèi)型也是函數(shù)脏嚷,且有一個(gè)參數(shù)骆撇,接收 Promise 執(zhí)行結(jié)果,同時(shí)可返回任意值父叙,作為新 Promise 的執(zhí)行結(jié)果
*/
then(onResolved, onRejected) {
// then 方法返回一個(gè)新的 Promise神郊,新 Promise 的狀態(tài)結(jié)果依賴(lài)于回調(diào)函數(shù)的返回值
return new Promise((resolve, reject) => {
// 對(duì)回調(diào)函數(shù)進(jìn)行一層封裝,主要是因?yàn)榛卣{(diào)函數(shù)的執(zhí)行結(jié)果會(huì)影響到返回的新 Promise 的狀態(tài)和結(jié)果
const _onResolved = () => {
// 根據(jù)回調(diào)函數(shù)的返回值趾唱,決定如何處理狀態(tài)變更
if (onResolved && onResolved instanceof Function) {
try {
const result = onResolved(this._value);
resolve(result);
} catch(e) {
reject(e);
}
} else {
// 如果傳入非函數(shù)類(lèi)型涌乳,則將上個(gè)Promise結(jié)果傳遞給下個(gè)處理
resolve(this._value);
}
};
const _onRejected = () => {
if (onRejected && onRejected instanceof Function) {
try {
const result = onRejected(this._value);
resolve(result);
} catch(e) {
reject(e);
}
} else {
reject(this._value);
}
};
// 如果當(dāng)前 Promise 狀態(tài)還沒(méi)變更,則將回調(diào)函數(shù)放入隊(duì)列里等待執(zhí)行
// 否則直接創(chuàng)建微任務(wù)來(lái)處理這些回調(diào)函數(shù)
if (this._status === this.PENDING) {
this._resolvedCallback.push(_onResolved);
this._rejectedCallback.push(_onRejected);
} else if (this._status === this.RESOLVED) {
setTimeout(_onResolved);
} else if (this._status === this.REJECTED) {
setTimeout(_onRejected);
}
});
}
其他方面
因?yàn)槟康脑谟诶砬?Promise 的主要功能職責(zé)甜癞,所以我的實(shí)現(xiàn)版并沒(méi)有按照規(guī)范一步步來(lái)夕晓,細(xì)節(jié)上,或者某些特殊場(chǎng)景的處理悠咱,可能欠缺考慮
比如對(duì)各個(gè)函數(shù)參數(shù)類(lèi)型的校驗(yàn)處理蒸辆,因?yàn)?Promise 的參數(shù)基本都是函數(shù)類(lèi)型征炼,但即使傳其他類(lèi)型,也仍舊不影響 Promise 的使用
比如為了避免被更改實(shí)現(xiàn)躬贡,一些內(nèi)部變量可以改用 Symbol 實(shí)現(xiàn)
但大體上谆奥,考慮了上面這些步驟實(shí)現(xiàn),基本功能也差不多了拂玻,重要的是狀態(tài)變更這個(gè)的處理要考慮全一點(diǎn)酸些,網(wǎng)上一些文章的實(shí)現(xiàn)版,這個(gè)是漏掉考慮的
還有當(dāng)面試遇到讓你手寫(xiě)實(shí)現(xiàn) Promise 時(shí)不要慌纺讲,可以按著這篇的思路擂仍,先把 Promise 的基本用法回顧一下,然后回想一下它支持的功能熬甚,再然后心里有個(gè)大概的骨架逢渔,其實(shí)無(wú)非也就是幾個(gè)內(nèi)部變量、構(gòu)造函數(shù)乡括、狀態(tài)變更函數(shù)肃廓、then 函數(shù)這幾塊而已,但死記硬背并不好诲泌,有個(gè)思路盲赊,一步步來(lái),總能回想起來(lái)
源碼
源碼補(bǔ)上了 catch敷扫,resolve 等其他方法的實(shí)現(xiàn)哀蘑,這些其實(shí)都是基于 Promise 基本功能上的一層封裝,方便使用
class Promise {
/**
* 構(gòu)造函數(shù)負(fù)責(zé)接收并執(zhí)行一個(gè) task 處理函數(shù)葵第,并將自己內(nèi)部提供的兩個(gè)狀態(tài)變更處理的函數(shù)傳遞給 task绘迁,同時(shí)將當(dāng)前 promise 狀態(tài)置為 PENDING(執(zhí)行中)
*/
constructor(task) {
/* 三種狀態(tài) */
this.PENDING = 'pending';
this.RESOLVED = 'resolved';
this.REJECTED = 'rejected';
/* 成功的回調(diào) */
this._resolvedCallback = [];
/* 失敗的回調(diào) */
this._rejectedCallback = [];
// 1. 將當(dāng)前狀態(tài)置為 PENDING
this._status = this.PENDING;
// 參數(shù)類(lèi)型校驗(yàn)
if (!(task instanceof Function)) {
throw new TypeError(`${task} is not a function`);
}
try {
// 2. 調(diào)用 task 處理函數(shù),并將狀態(tài)變更通知的函數(shù)傳遞過(guò)去卒密,需要注意 this 的處理
task(this._handleResolve.bind(this), this._handleReject.bind(this));
} catch (e) {
// 3. 如果 task 處理函數(shù)發(fā)生異常缀台,當(dāng)做失敗來(lái)處理
this._handleReject(e);
}
}
/**
* resolve 的狀態(tài)變更處理
*/
_handleResolve(value) {
if (this._status === this.PENDING) {
if (value instanceof Promise) {
// 1. 如果 value 是 Promise,那么等待 Promise 狀態(tài)結(jié)果出來(lái)后哮奇,再重新做狀態(tài)變更處理
try {
// 這里之所以不需要用 bind 來(lái)注意 this 問(wèn)題是因?yàn)槭褂昧思^函數(shù)
// 這里也可以寫(xiě)成 value.then(this._handleResole.bind(this), this._handleReject.bind(this))
value.then(v => {
this._handleResolve(v);
},
err => {
this._handleReject(err);
});
} catch(e) {
this._handleReject(e);
}
} else if (value && value.then instanceof Function) {
// 2. 如果 value 是具有 then 方法的對(duì)象時(shí)膛腐,那么將這個(gè) then 方法當(dāng)做 task 處理函數(shù),把狀態(tài)變更的觸發(fā)工作交由 then 來(lái)處理鼎俘,注意 this 的處理
try {
const then = value.then;
then.call(value, this._handleResolve.bind(this), this._handleReject.bind(this));
} catch(e) {
this._handleReject(e);
}
} else {
// 3. 其他類(lèi)型哲身,狀態(tài)變更、觸發(fā)成功的回調(diào)
this._status = this.RESOLVED;
this._value = value;
setTimeout(() => {
this._resolvedCallback.forEach(callback => {
callback();
});
});
}
}
}
/**
* reject 的狀態(tài)變更處理
*/
_handleReject(value) {
if (this._status === this.PENDING) {
this._status = this.REJECTED;
this._value = value;
setTimeout(() => {
this._rejectedCallback.forEach(callback => {
callback();
});
});
}
}
/**
* then 方法贸伐,接收兩個(gè)可選參數(shù)勘天,用于注冊(cè)回調(diào)處理,所以類(lèi)型也是函數(shù),且有一個(gè)參數(shù)误辑,接收 Promise 執(zhí)行結(jié)果沧踏,同時(shí)可返回任意值,作為新 Promise 的執(zhí)行結(jié)果
*/
then(onResolved, onRejected) {
// then 方法返回一個(gè)新的 Promise巾钉,新 Promise 的狀態(tài)結(jié)果依賴(lài)于回調(diào)函數(shù)的返回值
return new Promise((resolve, reject) => {
// 對(duì)回調(diào)函數(shù)進(jìn)行一層封裝翘狱,主要是因?yàn)榛卣{(diào)函數(shù)的執(zhí)行結(jié)果會(huì)影響到返回的新 Promise 的狀態(tài)和結(jié)果
const _onResolved = () => {
// 根據(jù)回調(diào)函數(shù)的返回值,決定如何處理狀態(tài)變更
if (onResolved && onResolved instanceof Function) {
try {
const result = onResolved(this._value);
resolve(result);
} catch(e) {
reject(e);
}
} else {
// 如果傳入非函數(shù)類(lèi)型砰苍,則將上個(gè)Promise結(jié)果傳遞給下個(gè)處理
resolve(this._value);
}
};
const _onRejected = () => {
if (onRejected && onRejected instanceof Function) {
try {
const result = onRejected(this._value);
resolve(result);
} catch(e) {
reject(e);
}
} else {
reject(this._value);
}
};
// 如果當(dāng)前 Promise 狀態(tài)還沒(méi)變更潦匈,則將回調(diào)函數(shù)放入隊(duì)列里等待執(zhí)行
// 否則直接創(chuàng)建微任務(wù)來(lái)處理這些回調(diào)函數(shù)
if (this._status === this.PENDING) {
this._resolvedCallback.push(_onResolved);
this._rejectedCallback.push(_onRejected);
} else if (this._status === this.RESOLVED) {
setTimeout(_onResolved);
} else if (this._status === this.REJECTED) {
setTimeout(_onRejected);
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof Promise) {
return value;
}
return new Promise((reso) => {
reso(value);
});
}
static reject(value) {
if (value instanceof Promise) {
return value;
}
return new Promise((reso, reje) => {
reje(value);
});
}
}
測(cè)試
網(wǎng)上有一些專(zhuān)門(mén)測(cè)試 Promise 的庫(kù),可以直接借助這些赚导,比如:promises-tests
我這里就舉一些基本功能的測(cè)試用例:
- 測(cè)試鏈?zhǔn)秸{(diào)用
// 測(cè)試鏈?zhǔn)秸{(diào)用
new Promise(r => {
console.log('0.--同步-----');
r();
}).then(v => console.log('1.-----------------'))
.then(v => console.log('2.-----------------'))
.then(v => console.log('3.-----------------'))
.then(v => console.log('4.-----------------'))
.then(v => console.log('5.-----------------'))
.then(v => console.log('6.-----------------'))
.then(v => console.log('7.-----------------'))
<details>
<summary>輸出</summary>
<pre><code>0.--同步-----
1.-----------------
2.-----------------
3.-----------------
4.-----------------
5.-----------------
6.-----------------
7.-----------------
</code></pre>
</details>
- 測(cè)試多次調(diào)用 then 注冊(cè)多個(gè)回調(diào)處理
// 測(cè)試多次調(diào)用 then 注冊(cè)多個(gè)回調(diào)處理
var p = new Promise(r => r(1));
p.then(v => console.log('1-----', v), err => console.error('error', err));
p.then(v => console.log('2-----', v), err => console.error('error', err));
p.then(v => console.log('3-----', v), err => console.error('error', err));
p.then(v => console.log('4-----', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>1----- 1
2----- 1
3----- 1
4----- 1
</code></pre>
</details>
- 測(cè)試異步場(chǎng)景
// 測(cè)試異步場(chǎng)景
new Promise(r => {
r(new Promise(a => setTimeout(a, 5000)).then(v => 1));
})
.then(v => {
console.log(v);
return new Promise(a => setTimeout(a, 1000)).then(v => 2);
})
.then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>1 // 5s 后才輸出
success 2 // 再2s后才輸出
</code></pre>
</details>
這個(gè)測(cè)試茬缩,可以檢測(cè)出 resolve 的狀態(tài)變更到底有沒(méi)有根據(jù)規(guī)范,區(qū)分不同場(chǎng)景進(jìn)行不同處理吼旧,你可以網(wǎng)上隨便找一篇 Promise 的實(shí)現(xiàn)凰锡,把它的代碼貼到瀏覽器的 console 里,然后測(cè)試一下看看圈暗,就知道有沒(méi)有問(wèn)題了
- 測(cè)試執(zhí)行結(jié)果類(lèi)型為 Promise 對(duì)象場(chǎng)景
// 測(cè)試執(zhí)行結(jié)果類(lèi)型為 Promise 對(duì)象場(chǎng)景(Promise 狀態(tài) 5s 后變化)
new Promise(r => {
r(new Promise(a => setTimeout(a, 5000)));
}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>success undefined // 5s 后才輸出
</code></pre>
</details>
// 測(cè)試執(zhí)行結(jié)果類(lèi)型為 Promise 對(duì)象場(chǎng)景(Promise 狀態(tài)不會(huì)發(fā)生變化)
new Promise(r => {
r(new Promise(a => 1));
}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>// 永遠(yuǎn)都不輸出
</code></pre>
</details>
- 測(cè)試執(zhí)行結(jié)果類(lèi)型為具有 then 方法的 thenable 對(duì)象場(chǎng)景
// 測(cè)試執(zhí)行結(jié)果類(lèi)型為具有 then 方法的 thenable 對(duì)象場(chǎng)景(then 方法內(nèi)部會(huì)調(diào)用傳遞的函數(shù)參數(shù))
new Promise(r => {
r({
then: (a, b) => {
return a(1);
}
});
}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>success 1
</code></pre>
</details>
// // 測(cè)試執(zhí)行結(jié)果類(lèi)型為具有 then 方法的 thenable 對(duì)象場(chǎng)景(then 方法內(nèi)部不會(huì)調(diào)用傳遞的函數(shù)參數(shù))
new Promise(r => {
r({
then: (a, b) => {
return 1;
}
});
}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>// 永遠(yuǎn)都不輸出
</code></pre>
</details>
// 測(cè)試執(zhí)行結(jié)果類(lèi)型為具有 then 的屬性掂为,但屬性值類(lèi)型非函數(shù)
new Promise(r => {
r({
then: 111
});
}).then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>success {then: 111}
</code></pre>
</details>
- 測(cè)試執(zhí)行結(jié)果的傳遞
// 測(cè)試當(dāng) Promise rejectd 時(shí),reject 的狀態(tài)結(jié)果會(huì)一直傳遞到可以處理這個(gè)失敗結(jié)果的那個(gè) then 的回調(diào)中
new Promise((r, j) => {
j(1);
}).then(v => console.log('success', v))
.then(v => console.log('success', v), err => console.error('error', err))
.catch(err => console.log('catch', err));
<details>
<summary>輸出</summary>
<pre><code>error 1
</code></pre>
</details>
// 測(cè)試傳給 then 的參數(shù)是非函數(shù)類(lèi)型時(shí)员串,執(zhí)行結(jié)果和狀態(tài)會(huì)一直傳遞
new Promise(r => {
r(1);
}).then(1)
.then(null, err => console.error('error', err))
.then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>success 1
</code></pre>
</details>
// 測(cè)試 rejectd 失敗被處理后勇哗,就不會(huì)繼續(xù)傳遞 rejectd
new Promise((r,j) => {
j(1);
}).then(2)
.then(v => console.log('success', v), err => console.error('error', err))
.then(v => console.log('success', v), err => console.error('error', err));
<details>
<summary>輸出</summary>
<pre><code>error 1
success undefined
</code></pre>
</details>
最后,當(dāng)你自己寫(xiě)完個(gè)模擬實(shí)現(xiàn) Promise 時(shí)寸齐,你可以將代碼貼到瀏覽器上欲诺,然后自己測(cè)試下這些用例,跟官方的 Promise 執(zhí)行結(jié)果比對(duì)下渺鹦,你就可以知道扰法,你實(shí)現(xiàn)的 Promise 基本功能上有沒(méi)有問(wèn)題了
當(dāng)然,需要更全面的測(cè)試的話(huà)海铆,還是得借助一些測(cè)試庫(kù)
不過(guò)迹恐,自己實(shí)現(xiàn)一個(gè) Promise 的目的其實(shí)也就在于理清 Promise 基本功能挣惰、行為卧斟、原理,所以這些用例能測(cè)通過(guò)的話(huà)憎茂,那么基本上也就掌握這些知識(shí)點(diǎn)了