完全理解 Promise 實(shí)現(xiàn)

完全理解 Promise 基本實(shí)現(xiàn)

網(wǎng)上有很多 Promise 實(shí)現(xiàn)方式留凭,看了都不是特別理解。
這里以一種更簡單的形式一步一步去理解/實(shí)現(xiàn)它杯道。這里僅涉及 Promise 構(gòu)造函數(shù)和 then 方法的實(shí)現(xiàn)

首先構(gòu)造一個(gè)最基本的 Promise 類

// version_1
class Promise {
    callbacks = [];
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        this.callbacks.forEach(callback => callback(value));
    }
}

// test
new Promise(resolve => {
    setTimeout(() => {
        console.log('await 2s');
        resolve('ok');
    }, 2000);
}).then((res) => {
    console.log('then', res);
})
  1. Promise 構(gòu)造函數(shù)會(huì)立即執(zhí)行用戶傳入的函數(shù) executor,并且把 _resolve 方法作為 executor 的參數(shù),傳給用戶處理
  2. 調(diào)用 then 方法(同步)赘艳,將 onFulfilled 放入callbacks隊(duì)列,其實(shí)也就是注冊(cè)回調(diào)函數(shù)克握,類似于觀察者模式蕾管。
  3. executor 模擬了異步,這里是過2s后執(zhí)行 resolve菩暗,對(duì)應(yīng)觸發(fā) _resolve 內(nèi)的 callbacks

.then(onFulfilled) 為何需要用一個(gè)數(shù)組存放掰曾?

then 方法可以調(diào)用多次,注冊(cè)的多個(gè)onFulfilled停团,并且這些 onFulfilled callbacks 會(huì)在異步操作完成(執(zhí)行resolve)后根據(jù)添加的順序依次執(zhí)行

// then 注冊(cè)多個(gè) onFulfilled 回調(diào)
const p = new Promise(resolve => {
    setTimeout(() => {
        console.log('await 2s');
        resolve('ok');
    }, 2000);
});

p.then(res => console.log('then1', res));
p.then(res => console.log('then2', res));
p.then(res => console.log('then3', res));

異步執(zhí)行處理 setTimeout vs status

上面 Promise 的實(shí)現(xiàn)存在一個(gè)問題:如果傳入的 executor 不是一個(gè)異步函數(shù)旷坦,resolve直接同步執(zhí)行,這時(shí) callbacks 還是空數(shù)組佑稠, 導(dǎo)致后面 then 方法注冊(cè)的 onFulfilled 回調(diào)就不會(huì)執(zhí)行(resolve 比 then 注冊(cè)先執(zhí)行)

// 同步執(zhí)行 resolve
new Promise(resolve => {
    console.log('同步執(zhí)行');
    resolve('同步執(zhí)行');
}).then(res => {
    console.log('then', res);
})

我們知道 then 中的回調(diào)總是通過異步執(zhí)行的秒梅,我們可以在 resolve 中加入 setTimeout,將 callbacks 的執(zhí)行時(shí)機(jī)放置到JS消息隊(duì)列舌胶,這樣 then方法的 onFulfilled 會(huì)先完成注冊(cè)捆蜀,再執(zhí)行消息隊(duì)列的 resolve

// version_2
class Promise {
    callbacks = [];
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        setTimeout(() => {
            this.callbacks.forEach(callback => callback(value));
        })
    }
}

但是這樣仍然有問題,如果我們延遲給 then 注冊(cè)回調(diào)幔嫂,這些回調(diào)也都無法執(zhí)行辆它。因?yàn)?br> 還是 resolve 先執(zhí)行完了,之后注冊(cè)的回調(diào)就無法執(zhí)行了履恩。

const p = new Promise(resolve => {
    console.log('同步執(zhí)行');
    resolve('同步執(zhí)行');
})

setTimeout(() => {
    p.then(res => {
        console.log('then', res); // never execute
    })
});

可以看出 setTimeout 是無法保證 then 注冊(cè)的 onFulfilled 正確執(zhí)行的锰茉,所以這里必須加入狀態(tài)機(jī)制(pending、fulfilled似袁、rejected)洞辣,且狀態(tài)只能由 pending 轉(zhuǎn)換為解決或拒絕。

// version_3:增加狀態(tài)機(jī)制
class Promise {
    callbacks = [];
    status = 'pending';
    value = undefined;
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.status === 'pending') {
            this.callbacks.push(onFulfilled);
        } else {
            onFulfilled(this.value);
        }
    }
    _resolve(value) {
        this.status = 'fulfilled';
        this.value = value;
        this.callbacks.forEach(callback => callback(value));
    }
}

當(dāng)增加了狀態(tài)后昙衅,setTimeout 就可以去掉了扬霜,狀態(tài)機(jī)制讓注冊(cè)的回調(diào)總是能正確工作。

  • 當(dāng) resolve 同步執(zhí)行時(shí)而涉,立即執(zhí)行 resolve著瓶,將 status 設(shè)置為 fulfilled ,并把 value 的值存起來啼县, 在此之后調(diào)用 then 添加的新回調(diào)材原,都會(huì)立即執(zhí)行
  • 當(dāng) resolve 異步執(zhí)行時(shí)沸久,pending 狀態(tài)執(zhí)行 then 會(huì)添加回調(diào)函數(shù), 等到 resolve 執(zhí)行時(shí)余蟹,回調(diào)函數(shù)會(huì)全部被執(zhí)行卷胯。

then的鏈?zhǔn)秸{(diào)用

鏈?zhǔn)秸{(diào)用我們可能很直接想到 then 方法中返回 this,這樣 Promise 實(shí)例就可以多次調(diào)用 then 方法威酒,但因?yàn)槭峭粋€(gè)實(shí)例窑睁,調(diào)用再多次 then 也只能返回相同的一個(gè)結(jié)果。而我們希望的鏈?zhǔn)秸{(diào)用應(yīng)該是這樣的:

new Promise(resolve => {
    resolve(1)
}).then(res => res + 2) // 1 + 2 = 3
    .then(res => res + 3) // 3 + 3 = 6
    .then(res => console.log(res)); // expected 6

每個(gè) then 注冊(cè)的 onFulfilled 都返回不同結(jié)果葵孤,并把結(jié)果傳給下一個(gè) onFulfilled 的參數(shù)担钮,所以 then 需要返回一個(gè)新的 Promise 實(shí)例

// version_4:then 的鏈?zhǔn)秸{(diào)用
class Promise {
    callbacks = [];
    status = 'pending';
    value = undefined;
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        return new Promise(resolveNext => {
            const fulfilled = (value) => {
                const results = onFulfilled(value); // 執(zhí)行 onFulfilled
                resolveNext(results); // 再執(zhí)行 resolveNext
            }
            if (this.status === 'pending') {
                this.callbacks.push(fulfilled);
            } else {
                fulfilled(this.value);
            }  
        })
    }
    _resolve(value) {
        this.status = 'fulfilled';
        this.value = value;
        this.callbacks.forEach(callback => callback(value));
    }
}

這樣一個(gè) Promise 就基本實(shí)現(xiàn)了,我們可以看到:

  • then 方法中尤仍,創(chuàng)建并返回了新的 Promise 實(shí)例箫津,這是串行 Promise 的基礎(chǔ)
  • 我們把 then 方法傳入的 形參 onFulfilled 以及創(chuàng)建新 Promise 實(shí)例時(shí)傳入的 resolveNext 合成一個(gè) 新函數(shù) fulfilled,這是銜接當(dāng)前 Promise 和后鄰 Promise 的關(guān)鍵所在

處理返回 Promise 類型的回調(diào)

這里還有一種特殊的情況:

  • resolve 方法傳入的參數(shù)為一個(gè) Promise 對(duì)象時(shí)
  • onFulfilled 方法返回一個(gè) Promise 對(duì)象時(shí)

這時(shí)我們只需用 res instanceof Promise 判斷處理下

// version_5:Promise 參數(shù)處理
class Promise {
    callbacks = [];
    status = 'pending';
    value = undefined;
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        return new Promise(resolveNext => {
            const fulfilled = (value) => {
                const results = onFulfilled(value);
                if (results instanceof Promise) {
                    // 如果當(dāng)前回調(diào)函數(shù)返回Promise對(duì)象宰啦,必須等待其狀態(tài)改變后在執(zhí)行下一個(gè)回調(diào)
                    results.then(resolveNext);
                } else {
                    // 否則會(huì)將返回結(jié)果直接作為參數(shù)苏遥,傳入下一個(gè)then的回調(diào)函數(shù),并立即執(zhí)行下一個(gè)then的回調(diào)函數(shù)
                    resolveNext(results);
                }
            }
            if (this.status === 'pending') {
                this.callbacks.push(fulfilled);
            } else {
                fulfilled(this.value);
            }  
        })
    }
    _resolve(value) {
        this.status = 'fulfilled';
        /**
         * 如果resolve的參數(shù)為Promise對(duì)象绑莺,則必須等待該P(yáng)romise對(duì)象狀態(tài)改變后,
         * 當(dāng)前Promsie的狀態(tài)才會(huì)改變暖眼,且狀態(tài)取決于參數(shù)Promsie對(duì)象的狀態(tài) 
        */
        if (value instanceof Promise) {
            value.then(nextValue => {
                this.value = nextValue;
                this.callbacks.forEach(callback => callback(value));
            })
        } else {
            this.value = value;
            this.callbacks.forEach(callback => callback(value));
        }
    }
}

拓展練習(xí)

嘗試實(shí)現(xiàn)下面函數(shù) LazyMan 的功能

LazyMan('Jack').sleep(3).eat('apple');
// Hi! Jack
// await 3s
// Jack eat apple~
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市纺裁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌司澎,老刑警劉巖欺缘,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異挤安,居然都是意外死亡谚殊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門蛤铜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫩絮,“玉大人,你說我怎么就攤上這事围肥〗烁桑” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵穆刻,是天一觀的道長置尔。 經(jīng)常有香客問我,道長氢伟,這世上最難降的妖魔是什么榜轿? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任幽歼,我火速辦了婚禮,結(jié)果婚禮上谬盐,老公的妹妹穿的比我還像新娘甸私。我一直安慰自己,他們只是感情好飞傀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布颠蕴。 她就那樣靜靜地躺著,像睡著了一般助析。 火紅的嫁衣襯著肌膚如雪犀被。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天外冀,我揣著相機(jī)與錄音寡键,去河邊找鬼。 笑死雪隧,一個(gè)胖子當(dāng)著我的面吹牛西轩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播脑沿,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼藕畔,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了庄拇?” 一聲冷哼從身側(cè)響起注服,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎措近,沒想到半個(gè)月后溶弟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瞭郑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年辜御,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屈张。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擒权,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出阁谆,到底是詐尸還是另有隱情碳抄,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布笛厦,位于F島的核電站纳鼎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贱鄙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一劝贸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逗宁,春花似錦映九、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哼拔,卻和暖如春引有,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背倦逐。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國打工譬正, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人檬姥。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓曾我,卻偏偏與公主長得像,于是被迫代替她去往敵國和親健民。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抒巢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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