圖解 Promise 實現(xiàn)原理(一)—— 基礎實現(xiàn)

本文首發(fā)于 vivo互聯(lián)網(wǎng)技術 微信公眾號
鏈接: https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ
作者:孔垂亮

很多同學在學習 Promise 時得滤,知其然卻不知其所以然捧韵,對其中的用法理解不了亭珍。本系列文章由淺入深逐步實現(xiàn) Promise屁柏,并結(jié)合流程圖闷串、實例以及動畫進行演示鸳址,達到深刻理解 Promise 用法的目的红且。

本系列文章有如下幾個章節(jié)組成:

  1. 圖解 Promise 實現(xiàn)原理(一)—— 基礎實現(xiàn)

  2. 圖解 Promise 實現(xiàn)原理(二)—— Promise 鏈式調(diào)用

  3. 圖解 Promise 實現(xiàn)原理(三)—— Promise 原型方法實現(xiàn)

  4. 圖解 Promise 實現(xiàn)原理(四)—— Promise 靜態(tài)方法實現(xiàn)

本文適合對 Promise 的用法有所了解的人閱讀,如果還不清楚片部,請自行查閱阮一峰老師的 《ES6入門 之 Promise 對象》镣衡。

Promise 規(guī)范有很多,如 Promise/A,Promise/B廊鸥,Promise/D 以及 Promise/A 的升級版 Promise/A+望浩,有興趣的可以去了解下,最終 ES6 中采用了 Promise/A+ 規(guī)范惰说。所以本文的Promise源碼是按照Promise/A+規(guī)范來編寫的(不想看英文版的移步Promise/A+規(guī)范中文翻譯)磨德。

引子

為了讓大家更容易理解,我們從一個場景開始吆视,一步一步跟著思路思考典挑,會更容易看懂。

考慮下面一種獲取用戶 id 的請求處理:

//不使用Promise        
http.get('some_url', function (result) {
    //do something
    console.log(result.id);
});

//使用Promise
new Promise(function (resolve) {
    //異步請求
    http.get('some_url', function (result) {
        resolve(result.id)
    })
}).then(function (id) {
    //do something
    console.log(id);
})

乍一看啦吧,好像不使用 Promise 更簡潔一些您觉。其實不然,設想一下丰滑,如果有好幾個依賴的前置請求都是異步的顾犹,此時如果沒有 Promise ,那回調(diào)函數(shù)要一層一層嵌套褒墨,看起來就很不舒服了炫刷。如下:

//不使用Promise        
http.get('some_url', function (id) {
    //do something
    http.get('getNameById', id, function (name) {
        //do something
        http.get('getCourseByName', name, function (course) {
            //dong something
            http.get('getCourseDetailByCourse', function (courseDetail) {
                //do something
            })
        })
    })
});

//使用Promise
function getUserId(url) {
    return new Promise(function (resolve) {
        //異步請求
        http.get(url, function (id) {
            resolve(id)
        })
    })
}
getUserId('some_url').then(function (id) {
    //do something
    return getNameById(id); // getNameById 是和 getUserId 一樣的Promise封裝。下同
}).then(function (name) {
    //do something
    return getCourseByName(name);
}).then(function (course) {
    //do something
    return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
    //do something
});

實現(xiàn)原理

說到底郁妈,Promise 也還是使用回調(diào)函數(shù)浑玛,只不過是把回調(diào)封裝在了內(nèi)部,使用上一直通過 then 方法的鏈式調(diào)用噩咪,使得多層的回調(diào)嵌套看起來變成了同一層的顾彰,書寫上以及理解上會更直觀和簡潔一些。

一胃碾、基礎版本

//極簡的實現(xiàn)
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

//Promise應用
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then((tip) => {
    console.log(tip);
})

上述代碼很簡單涨享,大致的邏輯是這樣的:

  1. 調(diào)用 then 方法,將想要在 Promise 異步操作成功時執(zhí)行的 onFulfilled 放入callbacks隊列仆百,其實也就是注冊回調(diào)函數(shù)厕隧,可以向觀察者模式方向思考;

  2. 創(chuàng)建 Promise 實例時傳入的函數(shù)會被賦予一個函數(shù)類型的參數(shù)俄周,即 resolve吁讨,它接收一個參數(shù) value,代表異步操作返回的結(jié)果峦朗,當異步操作執(zhí)行成功后建丧,會調(diào)用resolve方法,這時候其實真正執(zhí)行的操作是將 callbacks 隊列中的回調(diào)一一執(zhí)行波势。

image

(圖:基礎版本實現(xiàn)原理)

首先 new Promise 時翎朱,傳給 Promise 的函數(shù)設置定時器模擬異步的場景橄维,接著調(diào)用 Promise 對象的 then 方法注冊異步操作完成后的 onFulfilled,最后當異步操作完成時闭翩,調(diào)用 resolve(value)挣郭, 執(zhí)行 then 方法注冊的 onFulfilled。

then 方法注冊的 onFulfilled 是存在一個數(shù)組中疗韵,可見 then 方法可以調(diào)用多次,注冊的多個onFulfilled 會在異步操作完成后根據(jù)添加的順序依次執(zhí)行侄非。如下:

//then 的說明
let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
});

p.then(tip => {
    console.log('then1', tip);
});

p.then(tip => {
    console.log('then2', tip);
});

上例中蕉汪,要先定義一個變量 p ,然后 p.then 兩次逞怨。而規(guī)范中要求者疤,then 方法應該能夠鏈式調(diào)用。實現(xiàn)也簡單叠赦,只需要在 then 中 return this 即可驹马。如下所示:

//極簡的實現(xiàn)+鏈式調(diào)用
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
        return this;//看這里
    }
    _resolve(value) {
        this.callbacks.forEach(fn => fn(value));
    }
}

let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});
image

(圖:基礎版本的鏈式調(diào)用)

二、加入延遲機制

上面 Promise 的實現(xiàn)存在一個問題:如果在 then 方法注冊 onFulfilled 之前除秀,resolve 就執(zhí)行了糯累,onFulfilled 就不會執(zhí)行到了。比如上面的例子中我們把 setTimout 去掉:

//同步執(zhí)行了resolve
let p = new Promise(resolve => {
    console.log('同步執(zhí)行');
    resolve('同步執(zhí)行');
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

執(zhí)行結(jié)果顯示册踩,只有 "同步執(zhí)行" 被打印了出來泳姐,后面的 "then1" 和 "then2" 均沒有打印出來。再回去看下 Promise 的源碼暂吉,也很好理解胖秒,resolve 執(zhí)行時,callbacks 還是空數(shù)組慕的,還沒有onFulfilled 注冊上來阎肝。

這顯然是不允許的,Promises/A+規(guī)范明確要求回調(diào)需要通過異步方式執(zhí)行肮街,用以保證一致可靠的執(zhí)行順序风题。因此要加入一些處理,保證在 resolve 執(zhí)行之前低散,then 方法已經(jīng)注冊完所有的回調(diào):

//極簡的實現(xiàn)+鏈式調(diào)用+延遲機制
class Promise {
    callbacks = [];
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
        return this;
    }
    _resolve(value) {
        setTimeout(() => {//看這里
            this.callbacks.forEach(fn => fn(value));
        });
    }
}

在 resolve 中增加定時器俯邓,通過 setTimeout 機制,將 resolve 中執(zhí)行回調(diào)的邏輯放置到JS任務隊列末尾熔号,以保證在 resolve 執(zhí)行時稽鞭,then方法的 onFulfilled 已經(jīng)注冊完成。

image

(圖:延遲機制)

但是這樣依然存在問題引镊,在 resolve 執(zhí)行后朦蕴,再通過 then 注冊上來的 onFulfilled 都沒有機會執(zhí)行了篮条。如下所示,我們加了延遲后吩抓,then1 和 then2 可以打印出來了涉茧,但下例中的 then3 依然打印不出來。所以我們需要增加狀態(tài)疹娶,并且保存 resolve 的值伴栓。

let p = new Promise(resolve => {
    console.log('同步執(zhí)行');
    resolve('同步執(zhí)行');
}).then(tip => {
    console.log('then1', tip);
}).then(tip => {
    console.log('then2', tip);
});

setTimeout(() => {
    p.then(tip => {
        console.log('then3', tip);
    })
});

三、增加狀態(tài)

為了解決上一節(jié)拋出的問題雨饺,我們必須加入狀態(tài)機制钳垮,也就是大家熟知的 pending、fulfilled额港、rejected饺窿。

Promises/A+ 規(guī)范中明確規(guī)定了,pending 可以轉(zhuǎn)化為 fulfilled 或 rejected 并且只能轉(zhuǎn)化一次移斩,也就是說如果 pending 轉(zhuǎn)化到 fulfilled 狀態(tài)肚医,那么就不能再轉(zhuǎn)化到 rejected。并且 fulfilled 和 rejected 狀態(tài)只能由 pending 轉(zhuǎn)化而來向瓷,兩者之間不能互相轉(zhuǎn)換肠套。

image

增加狀態(tài)后的實現(xiàn)是這樣的

//極簡的實現(xiàn)+鏈式調(diào)用+延遲機制+狀態(tài)
class Promise {
    callbacks = [];
    state = 'pending';//增加狀態(tài)
    value = null;//保存結(jié)果
    constructor(fn) {
        fn(this._resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.state === 'pending') {//在resolve之前,跟之前邏輯一樣风罩,添加到callbacks中
            this.callbacks.push(onFulfilled);
        } else {//在resolve之后糠排,直接執(zhí)行回調(diào),返回結(jié)果了
            onFulfilled(this.value);
        }
        return this;
    }
    _resolve(value) {
        this.state = 'fulfilled';//改變狀態(tài)
        this.value = value;//保存結(jié)果
        this.callbacks.forEach(fn => fn(value));
    }
}

注意:當增加完狀態(tài)之后超升,原先的_resolve中的定時器可以去掉了入宦。當reolve同步執(zhí)行時,雖然callbacks為空室琢,回調(diào)函數(shù)還沒有注冊上來乾闰,但沒有關系,因為后面注冊上來時盈滴,判斷狀態(tài)為fulfilled涯肩,會立即執(zhí)行回調(diào)。

image
image

(圖:Promise 狀態(tài)管理)

實現(xiàn)源碼中只增加了 fulfilled 的狀態(tài) 和 onFulfilled 的回調(diào)巢钓,但為了完整性病苗,在示意圖中增加了 rejected 和 onRejected 。后面章節(jié)會實現(xiàn)症汹。

resolve 執(zhí)行時硫朦,會將狀態(tài)設置為 fulfilled ,并把 value 的值存起來背镇,在此之后調(diào)用 then 添加的新回調(diào)咬展,都會立即執(zhí)行泽裳,直接返回保存的value值。

(Promise 狀態(tài)變化演示動畫)

詳情請點擊: https://mp.weixin.qq.com/s/UNzYgpnKzmW6bAapYxnXRQ

至此破婆,一個初具功能的Promise就實現(xiàn)好了涮总,它實現(xiàn)了 then,實現(xiàn)了鏈式調(diào)用祷舀,實現(xiàn)了狀態(tài)管理等等瀑梗。但仔細想想,鏈式調(diào)用的實現(xiàn)只是在 then 中 return 了 this裳扯,因為是同一個實例夺克,調(diào)用再多次 then 也只能返回相同的一個結(jié)果,這顯然是不能滿足我們的要求的嚎朽。下一節(jié),講述如何實現(xiàn)真正的鏈式調(diào)用柬帕。

更多內(nèi)容敬請關注 vivo 互聯(lián)網(wǎng)技術 微信公眾號

image

注:轉(zhuǎn)載文章請先與微信號:labs2020 聯(lián)系哟忍。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市陷寝,隨后出現(xiàn)的幾起案子锅很,更是在濱河造成了極大的恐慌,老刑警劉巖凤跑,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爆安,死亡現(xiàn)場離奇詭異,居然都是意外死亡仔引,警方通過查閱死者的電腦和手機扔仓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咖耘,“玉大人翘簇,你說我怎么就攤上這事《梗” “怎么了版保?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長夫否。 經(jīng)常有香客問我彻犁,道長,這世上最難降的妖魔是什么凰慈? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任汞幢,我火速辦了婚禮,結(jié)果婚禮上溉瓶,老公的妹妹穿的比我還像新娘急鳄。我一直安慰自己谤民,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布疾宏。 她就那樣靜靜地躺著张足,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坎藐。 梳的紋絲不亂的頭發(fā)上为牍,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音岩馍,去河邊找鬼碉咆。 笑死,一個胖子當著我的面吹牛蛀恩,可吹牛的內(nèi)容都是我干的疫铜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼双谆,長吁一口氣:“原來是場噩夢啊……” “哼壳咕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起顽馋,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤谓厘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后寸谜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竟稳,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年熊痴,在試婚紗的時候發(fā)現(xiàn)自己被綠了他爸。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡愁拭,死狀恐怖讲逛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情岭埠,我是刑警寧澤盏混,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站惜论,受9級特大地震影響许赃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜馆类,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一混聊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧乾巧,春花似錦句喜、人聲如沸预愤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽植康。三九已至侦高,卻和暖如春掰茶,著一層夾襖步出監(jiān)牢的瞬間教届,已是汗流浹背妇穴。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荸实,地道東北人济丘。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓棚唆,卻偏偏與公主長得像来惧,于是被迫代替她去往敵國和親冗栗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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