模擬實(shí)現(xiàn) Promise(小白版)

模擬實(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)鸽嫂,思考下,都需要哪些代碼:

  1. 變量上至少需要:三種狀態(tài)征讲、當(dāng)前狀態(tài)(_status)据某、傳遞給回調(diào)函數(shù)的結(jié)果值(_value)
  2. 構(gòu)造函數(shù) constructor
  3. task 處理函數(shù)
  4. task 處理函數(shù)的兩個(gè)用于通知狀態(tài)變更的函數(shù)(handleResolve, handleReject)
  5. then 方法
  6. then 方法注冊(cè)的兩個(gè)回調(diào)函數(shù)
  7. 回調(diào)函數(shù)隊(duì)列
  8. 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ì)列
  • 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ì)列

所以你可以看到,其實(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)了

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末珍语,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子竖幔,更是在濱河造成了極大的恐慌板乙,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異募逞,居然都是意外死亡蛋铆,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)放接,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)刺啦,“玉大人,你說(shuō)我怎么就攤上這事纠脾÷耆常” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵苟蹈,是天一觀(guān)的道長(zhǎng)糊渊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)慧脱,這世上最難降的妖魔是什么渺绒? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮菱鸥,結(jié)果婚禮上芒篷,老公的妹妹穿的比我還像新娘。我一直安慰自己采缚,他們只是感情好针炉,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著扳抽,像睡著了一般篡帕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贸呢,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天镰烧,我揣著相機(jī)與錄音,去河邊找鬼楞陷。 笑死怔鳖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的固蛾。 我是一名探鬼主播结执,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼艾凯!你這毒婦竟也來(lái)了献幔?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤趾诗,失蹤者是張志新(化名)和其女友劉穎蜡感,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡郑兴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年犀斋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片情连。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闪水,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒙具,到底是詐尸還是另有隱情球榆,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布禁筏,位于F島的核電站持钉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏篱昔。R本人自食惡果不足惜每强,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望州刽。 院中可真熱鬧空执,春花似錦、人聲如沸穗椅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)匹表。三九已至门坷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袍镀,已是汗流浹背默蚌。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苇羡,地道東北人绸吸。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像设江,于是被迫代替她去往敵國(guó)和親锦茁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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