30分鐘银室,讓你徹底明白Promise原理

前言

前一陣子記錄了promise的一些常規(guī)用法蝎抽,這篇文章再深入一個層次,來分析分析promise的這種規(guī)則機制是如何實現(xiàn)的缚去。ps:本文適合已經(jīng)對promise的用法有所了解的人閱讀潮秘。

本文的promise源碼是按照Promise/A+規(guī)范來編寫的(不想看英文版的移步Promise/A+規(guī)范中文翻譯

引子

為了讓大家更容易理解,我們從一個場景開始講解易结,讓大家一步一步跟著思路思考枕荞,相信你一定會更容易看懂。

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

//例1
function getUserId() {
    return new Promise(function(resolve) {
        //異步請求
        http.get(url, function(results) {
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些處理
})

getUserId方法返回一個promise搞动,可以通過它的then方法注冊(注意注冊這個詞)在promise異步操作成功時執(zhí)行的回調躏精。這種執(zhí)行方式,使得異步調用變得十分順手鹦肿。

原理剖析

那么類似這種功能的Promise怎么實現(xiàn)呢矗烛?其實按照上面一句話,實現(xiàn)一個最基礎的雛形還是很easy的箩溃。

極簡promise雛形

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks為數(shù)組瞭吃,因為可能同時有很多個回調

    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

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

  1. 調用then方法涣旨,將想要在Promise異步操作成功時執(zhí)行的回調放入callbacks隊列虱而,其實也就是注冊回調函數(shù),可以向觀察者模式方向思考开泽;

  2. 創(chuàng)建Promise實例時傳入的函數(shù)會被賦予一個函數(shù)類型的參數(shù)牡拇,即resolve,它接收一個參數(shù)value穆律,代表異步操作返回的結果惠呼,當一步操作執(zhí)行成功后,用戶會調用resolve方法峦耘,這時候其實真正執(zhí)行的操作是將callbacks隊列中的回調一一執(zhí)行剔蹋;

可以結合例1中的代碼來看,首先new Promise時辅髓,傳給promise的函數(shù)發(fā)送異步請求泣崩,接著調用promise對象的then屬性少梁,注冊請求成功的回調函數(shù),然后當異步請求發(fā)送成功時矫付,調用resolve(results.id)方法, 該方法執(zhí)行then方法注冊的回調數(shù)組凯沪。

相信仔細的人應該可以看出來,then方法應該能夠鏈式調用买优,但是上面的最基礎簡單的版本顯然無法支持鏈式調用妨马。想讓then方法支持鏈式調用,其實也是很簡單的:

this.then = function (onFulfilled) {
    callbacks.push(onFulfilled);
    return this;
};

see?只要簡單一句話就可以實現(xiàn)類似下面的鏈式調用:

// 例2
function getUserId() {
    return new Promise(function (resolve) {
        resolve(9876);
    });
}
getUserId().then(function (id) {
    // 一些處理
}).then(function (id) {
    // 一些處理
});

加入延時機制

細心的同學應該發(fā)現(xiàn)杀赢,上述代碼可能還存在一個問題:如果在then方法注冊回調之前烘跺,resolve函數(shù)就執(zhí)行了,怎么辦脂崔?比如promise內(nèi)部的函數(shù)是同步函數(shù):

// 例3
function getUserId() {
    return new Promise(function (resolve) {
        resolve(9876);
    });
}
getUserId().then(function (id) {
    // 一些處理
});

這顯然是不允許的滤淳,Promises/A+規(guī)范明確要求回調需要通過異步方式執(zhí)行,用以保證一致可靠的執(zhí)行順序砌左。因此我們要加入一些處理脖咐,保證在resolve執(zhí)行之前,then方法已經(jīng)注冊完所有的回調绊困。我們可以這樣改造下resolve函數(shù):

function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)
}

上述代碼的思路也很簡單文搂,就是通過setTimeout機制适刀,將resolve中執(zhí)行回調的邏輯放置到JS任務隊列末尾秤朗,以保證在resolve執(zhí)行時,then方法的回調函數(shù)已經(jīng)注冊完成.

但是笔喉,這樣好像還存在一個問題取视,可以細想一下:如果Promise異步操作已經(jīng)成功,這時常挚,在異步操作成功之前注冊的回調都會執(zhí)行作谭,但是在Promise異步操作成功這之后調用的then注冊的回調就再也不會執(zhí)行了,這顯然不是我們想要的奄毡。

加入狀態(tài)

恩折欠,為了解決上一節(jié)拋出的問題,我們必須加入狀態(tài)機制吼过,也就是大家熟知的pending锐秦、fulfilledrejected盗忱。

Promises/A+規(guī)范中的2.1Promise States中明確規(guī)定了酱床,pending可以轉化為fulfilledrejected并且只能轉化一次,也就是說如果pending轉化到fulfilled狀態(tài)趟佃,那么就不能再轉化到rejected扇谣。并且fulfilledrejected狀態(tài)只能由pending轉化而來昧捷,兩者之間不能互相轉換。一圖勝千言:

改進后的代碼是這樣的:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };

    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }

    fn(resolve);
}

上述代碼的思路是這樣的:resolve執(zhí)行時罐寨,會將狀態(tài)設置為fulfilled靡挥,在此之后調用then添加的新回調,都會立即執(zhí)行衩茸。

這里沒有任何地方將state設為rejected芹血,為了讓大家聚焦在核心代碼上,這個問題后面會有一小節(jié)專門加入楞慈。

鏈式Promise

那么這里問題又來了幔烛,如果用戶再then函數(shù)里面注冊的仍然是一個Promise,該如何解決囊蓝?比如下面的例4

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 對job的處理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}

這種場景相信用過promise的人都知道會有很多饿悬,那么類似這種就是所謂的鏈式Promise

鏈式Promise是指在當前promise達到fulfilled狀態(tài)后聚霜,即開始進行下一個promise(后鄰promise)狡恬。那么我們?nèi)绾毋暯赢斍?code>promise和后鄰promise呢?(這是這里的難點)蝎宇。

其實也不是辣么難弟劲,只要在then方法里面return一個promise就好啦。Promises/A+規(guī)范中的2.2.7就是這么說噠(微笑臉)~

下面來看看這段暗藏玄機的then方法和resolve方法改造代碼:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        //如果then中沒有傳遞任何東西
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }

        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve);
}

我們結合例4的代碼姥芥,分析下上面的代碼邏輯兔乞,為了方便閱讀,我把例4的代碼貼在這里:

// 例4
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 對job的處理
    });

function getUserJobById(id) {
    return new Promise(function (resolve) {
        http.get(baseUrl + id, function(job) {
            resolve(job);
        });
    });
}
  1. then方法中凉唐,創(chuàng)建并返回了新的Promise實例庸追,這是串行Promise的基礎,并且支持鏈式調用台囱。

  2. handle方法是promise內(nèi)部的方法淡溯。then方法傳入的形參onFulfilled以及創(chuàng)建新Promise實例時傳入的resolve均被push到當前promisecallbacks隊列中,這是銜接當前promise和后鄰promise的關鍵所在(這里一定要好好的分析下handle的作用)簿训。

  3. getUserId生成的promise(簡稱getUserId promise)異步操作成功咱娶,執(zhí)行其內(nèi)部方法resolve,傳入的參數(shù)正是異步操作的結果id

  4. 調用handle方法處理callbacks隊列中的回調:getUserJobById方法强品,生成新的promisegetUserJobById promise

  5. 執(zhí)行之前由getUserId promisethen方法生成的新promise(稱為bridge promise)的resolve方法膘侮,傳入?yún)?shù)為getUserJobById promise。這種情況下择懂,會將該resolve方法傳入getUserJobById promisethen方法中喻喳,并直接返回。

  6. getUserJobById promise異步操作成功時困曙,執(zhí)行其callbacks中的回調:getUserId bridge promise中的resolve方法

  7. 最后執(zhí)行getUserId bridge promise的后鄰promisecallbacks中的回調表伦。
    更直白的可以看下面的圖谦去,一圖勝千言(都是根據(jù)自己的理解畫出來的,如有不對歡迎指正):

失敗處理

在異步操作失敗時蹦哼,標記其狀態(tài)為rejected鳄哭,并執(zhí)行注冊的失敗回調:

//例5
function getUserId() {
    return new Promise(function(resolve) {
        //異步請求
        http.get(url, function(error, results) {
            if (error) {
                reject(error);
            }
            resolve(results.id)
        })
    })
}

getUserId().then(function(id) {
    //一些處理
}, function(error) {
    console.log(error)
})

有了之前處理fulfilled狀態(tài)的經(jīng)驗,支持錯誤處理變得很容易,只需要在注冊回調纲熏、處理狀態(tài)變更上都要加入新的邏輯:

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];

    this.then = function (onFulfilled, onRejected) {
        return new Promise(function (resolve, reject) {
            handle({
                onFulfilled: onFulfilled || null,
                onRejected: onRejected || null,
                resolve: resolve,
                reject: reject
            });
        });
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }

        var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
            ret;
        if (cb === null) {
            cb = state === 'fulfilled' ? callback.resolve : callback.reject;
            cb(value);
            return;
        }
        ret = cb(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve, reject);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        execute();
    }

    function reject(reason) {
        state = 'rejected';
        value = reason;
        execute();
    }

    function execute() {
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    fn(resolve, reject);
}

上述代碼增加了新的reject方法妆丘,供異步操作失敗時調用,同時抽出了resolvereject共用的部分局劲,形成execute方法勺拣。

錯誤冒泡是上述代碼已經(jīng)支持,且非常實用的一個特性鱼填。在handle中發(fā)現(xiàn)沒有指定異步操作失敗的回調時药有,會直接將bridge promise(then函數(shù)返回的promise,后同)設為rejected狀態(tài)苹丸,如此達成執(zhí)行后續(xù)失敗回調的效果愤惰。這有利于簡化串行Promise的失敗處理成本,因為一組異步操作往往會對應一個實際功能赘理,失敗處理方法通常是一致的:

//例6
getUserId()
    .then(getUserJobById)
    .then(function (job) {
        // 處理job
    }, function (error) {
        // getUserId或者getUerJobById時出現(xiàn)的錯誤
        console.log(error);
    });

異常處理

細心的同學會想到:如果在執(zhí)行成功回調宦言、失敗回調時代碼出錯怎么辦?對于這類異常商模,可以使用try-catch捕獲錯誤奠旺,并將bridge promise設為rejected狀態(tài)。handle方法改造如下:

function handle(callback) {
    if (state === 'pending') {
        callbacks.push(callback);
        return;
    }

    var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
        ret;
    if (cb === null) {
        cb = state === 'fulfilled' ? callback.resolve : callback.reject;
        cb(value);
        return;
    }
    try {
        ret = cb(value);
        callback.resolve(ret);
    } catch (e) {
        callback.reject(e);
    } 
}

如果在異步操作中阻桅,多次執(zhí)行resolve或者reject會重復處理后續(xù)回調凉倚,可以通過內(nèi)置一個標志位解決兼都。

總結

剛開始看promise源碼的時候總不能很好的理解then和resolve函數(shù)的運行機理嫂沉,但是如果你靜下心來,反過來根據(jù)執(zhí)行promise時的邏輯來推演扮碧,就不難理解了趟章。這里一定要注意的點是:promise里面的then函數(shù)僅僅是注冊了后續(xù)需要執(zhí)行的代碼,真正的執(zhí)行是在resolve方法里面執(zhí)行的慎王,理清了這層蚓土,再來分析源碼會省力的多。

現(xiàn)在回顧下Promise的實現(xiàn)過程赖淤,其主要使用了設計模式中的觀察者模式:

  1. 通過Promise.prototype.then和Promise.prototype.catch方法將觀察者方法注冊到被觀察者Promise對象中蜀漆,同時返回一個新的Promise對象,以便可以鏈式調用咱旱。

  2. 被觀察者管理內(nèi)部pending确丢、fulfilled和rejected的狀態(tài)轉變绷耍,同時通過構造函數(shù)中傳遞的resolve和reject方法以主動觸發(fā)狀態(tài)轉變和通知觀察者。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲜侥,一起剝皮案震驚了整個濱河市褂始,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌描函,老刑警劉巖崎苗,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異舀寓,居然都是意外死亡胆数,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門互墓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幅慌,“玉大人,你說我怎么就攤上這事轰豆∫任椋” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵酸休,是天一觀的道長骂租。 經(jīng)常有香客問我,道長斑司,這世上最難降的妖魔是什么渗饮? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮宿刮,結果婚禮上互站,老公的妹妹穿的比我還像新娘。我一直安慰自己僵缺,他們只是感情好胡桃,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著磕潮,像睡著了一般翠胰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上自脯,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天之景,我揣著相機與錄音,去河邊找鬼膏潮。 笑死锻狗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播轻纪,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼脚囊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了桐磁?” 一聲冷哼從身側響起悔耘,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎我擂,沒想到半個月后衬以,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡校摩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年看峻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衙吩。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡互妓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出坤塞,到底是詐尸還是另有隱情冯勉,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布摹芙,位于F島的核電站灼狰,受9級特大地震影響,放射性物質發(fā)生泄漏浮禾。R本人自食惡果不足惜交胚,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盈电。 院中可真熱鬧蝴簇,春花似錦、人聲如沸匆帚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卷扮。三九已至荡澎,卻和暖如春均践,著一層夾襖步出監(jiān)牢的瞬間晤锹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工彤委, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留鞭铆,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像车遂,于是被迫代替她去往敵國和親封断。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

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

  • Promise 對象 Promise 的含義 Promise 是異步編程的一種解決方案舶担,比傳統(tǒng)的解決方案——回調函...
    neromous閱讀 8,703評論 1 56
  • 一坡疼、Promise的含義 Promise在JavaScript語言中早有實現(xiàn),ES6將其寫進了語言標準衣陶,統(tǒng)一了用法...
    Alex灌湯貓閱讀 820評論 0 2
  • 夜像神秘幽暗的湖 人似湖中冷漠的魚 凝滯的黑暗中試探觸碰 寒冷的沉默里相依相偎 每當黎明到來 湖光就變得迷人清澈 ...
    藍衣客閱讀 437評論 0 4
  • 判斷當前網(wǎng)頁是不是微信打開 方法一(微信官網(wǎng)): 方法二: 方法三: 方法四(PHP判斷): 判斷當前設備是不是A...
    郝特么冷閱讀 250評論 2 2