天天看點

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

本文來自網易雲社群。

前一陣子記錄了promise的一些正常用法,這篇文章再深入一個層次,來分析分析promise的這種規則機制是如何實作的。ps:本文适合已經對promise的用法有所了解的人閱讀,如果對其用法還不是太了解,可以移步我的上一篇博文。

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

引子

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

考慮下面一種擷取使用者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

異步操作成功時執行的回調。這種執行方式,使得異步調用變得十分順手。

原理剖析

那麼類似這種功能的

Promise

怎麼實作呢?其實按照上面一句話,實作一個最基礎的雛形還是很easy的。

極簡promise雛形

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

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

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

    fn(resolve);
}
           

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

  1. 調用

    then

    方法,将想要在

    Promise

    異步操作成功時執行的回調放入

    callbacks

    隊列,其實也就是注冊回調函數,可以向觀察者模式方向思考;
  2. 建立

    Promise

    執行個體時傳入的函數會被賦予一個函數類型的參數,即

    resolve

    ,它接收一個參數value,代表異步操作傳回的結果,當一步操作執行成功後,使用者會調用

    resolve

    方法,這時候其實真正執行的操作是将

    callbacks

    隊列中的回調一一執行;

可以結合

例1

中的代碼來看,首先

new Promise

時,傳給

promise

的函數發送異步請求,接着調用

promise

對象的

then

屬性,注冊請求成功的回調函數,然後當異步請求發送成功時,調用

resolve(results.id)

方法, 該方法執行

then

方法注冊的回調數組。

相信仔細的人應該可以看出來,

then

方法應該能夠鍊式調用,但是上面的最基礎簡單的版本顯然無法支援鍊式調用。想讓

then

方法支援鍊式調用,其實也是很簡單的:

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

see?隻要簡單一句話就可以實作類似下面的鍊式調用:

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

加入延時機制

細心的同學應該發現,上述代碼可能還存在一個問題:如果在

then

方法注冊回調之前,

resolve

函數就執行了,怎麼辦?比如

promise

内部的函數是同步函數:

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

這顯然是不允許的,

Promises/A+

規範明确要求回調需要通過異步方式執行,用以保證一緻可靠的執行順序。是以我們要加入一些處理,保證在

resolve

執行之前,

then

方法已經注冊完所有的回調。我們可以這樣改造下

resolve

函數:

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

上述代碼的思路也很簡單,就是通過

setTimeout

機制,将

resolve

中執行回調的邏輯放置到

JS

任務隊列末尾,以保證在

resolve

執行時,

then

方法的回調函數已經注冊完成.

但是,這樣好像還存在一個問題,可以細想一下:如果

Promise

異步操作已經成功,這時,在異步操作成功之前注冊的回調都會執行,但是在

Promise

異步操作成功這之後調用的

then

注冊的回調就再也不會執行了,這顯然不是我們想要的。

加入狀态

恩,為了解決上一節抛出的問題,我們必須加入狀态機制,也就是大家熟知的

pending

fulfilled

rejected

Promises/A+

規範中的2.1

Promise States

中明确規定了,

pending

可以轉化為

fulfilled

rejected

并且隻能轉化一次,也就是說如果

pending

轉化到

fulfilled

狀态,那麼就不能再轉化到

rejected

。并且

fulfilled

rejected

狀态隻能由

pending

轉化而來,兩者之間不能互相轉換。一圖勝千言:

<img src="https://pic4.zhimg.com/v2-a90a6c703c051370aee8311ec00ef4a5_b.jpg" data-caption="" data-size="normal" data-rawwidth="353" data-rawheight="260" class="content_image" width="353">

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

改進後的代碼是這樣的:

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

執行時,會将狀态設定為

fulfilled

,在此之後調用

then

添加的新回調,都會立即執行。

這裡沒有任何地方将

state

設為

rejected

,為了讓大家聚焦在核心代碼上,這個問題後面會有一小節專門加入。

鍊式Promise

那麼這裡問題又來了,如果使用者再then函數裡面注冊的仍然是一個

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

狀态後,即開始進行下一個

promise

(後鄰

promise

)。那麼我們如何銜接目前

promise

和後鄰

promise

呢?(這是這裡的難點)。

其實也不是辣麼難,隻要在

then

方法裡面

return

一個

promise

就好啦。

Promises/A+

規範中的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

    方法中,建立并傳回了新的

    Promise

    執行個體,這是串行

    Promise

    的基礎,并且支援鍊式調用。
  2. handle

    方法是

    promise

    内部的方法。

    then

    方法傳入的形參

    onFulfilled

    以及建立新

    Promise

    執行個體時傳入的

    resolve

    均被

    push

    到目前

    promise

    callbacks

    隊列中,這是銜接目前

    promise

    promise

    的關鍵所在(這裡一定要好好的分析下handle的作用)。
  3. getUserId

    生成的

    promise

    (簡稱

    getUserId promise

    )異步操作成功,執行其内部方法

    resolve

    ,傳入的參數正是異步操作的結果

    id

  4. handle

    方法處理

    callbacks

    隊列中的回調:

    getUserJobById

    方法,生成新的

    promise

    getUserJobById promise

  5. 執行之前由

    getUserId promise

    then

    方法生成的新

    promise

    (稱為

    bridge promise

    )的

    resolve

    方法,傳入參數為

    getUserJobById promise

    。這種情況下,會将該

    resolve

    方法傳入

    getUserJobById promise

    then

    方法中,并直接傳回。
  6. getUserJobById promise

    異步操作成功時,執行其

    callbacks

    中的回調:

    getUserId bridge promise

    中的

    resolve

    方法
  7. 最後執行

    getUserId bridge promise

    的後鄰

    promise

    callbacks

    中的回調。

更直白的可以看下面的圖,一圖勝千言(都是根據自己的了解畫出來的,如有不對歡迎指正):

<img src="https://pic3.zhimg.com/v2-73684d65d37e4179096f60b004785263_b.jpg" data-caption="" data-size="normal" data-rawwidth="2137" data-rawheight="1262" class="origin_image zh-lightbox-thumb" width="2137" data-original="https://pic3.zhimg.com/v2-73684d65d37e4179096f60b004785263_r.jpg">

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

失敗處理

在異步操作失敗時,标記其狀态為

rejected

,并執行注冊的失敗回調:

//例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

狀态的經驗,支援錯誤處理變得很容易,隻需要在注冊回調、處理狀态變更上都要加入新的邏輯:

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

方法,供異步操作失敗時調用,同時抽出了

resolve

reject

共用的部分,形成

execute

方法。

錯誤冒泡是上述代碼已經支援,且非常實用的一個特性。在

handle

中發現沒有指定異步操作失敗的回調時,會直接将

bridge promise

(

then

函數傳回的

promise

,後同)設為

rejected

狀态,如此達成執行後續失敗回調的效果。這有利于簡化串行

Promise

的失敗處理成本,因為一組異步操作往往會對應一個實際功能,失敗處理方法通常是一緻的:

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

異常處理

細心的同學會想到:如果在執行成功回調、失敗回調時代碼出錯怎麼辦?對于這類異常,可以使用

try-catch

捕獲錯誤,并将

bridge promise

rejected

狀态。

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);
    } 
}
           

如果在異步操作中,多次執行

resolve

或者

reject

會重複處理後續回調,可以通過内置一個标志位解決。

總結

剛開始看promise源碼的時候總不能很好的了解then和resolve函數的運作機理,但是如果你靜下心來,反過來根據執行promise時的邏輯來推演,就不難了解了。這裡一定要注意的點是:promise裡面的then函數僅僅是注冊了後續需要執行的代碼,真正的執行是在resolve方法裡面執行的,理清了這層,再來分析源碼會省力的多。

現在回顧下Promise的實作過程,其主要使用了設計模式中的觀察者模式:

  1. 通過Promise.prototype.then和Promise.prototype.catch方法将觀察者方法注冊到被觀察者Promise對象中,同時傳回一個新的Promise對象,以便可以鍊式調用。
  2. 被觀察者管理内部pending、fulfilled和rejected的狀态轉變,同時通過構造函數中傳遞的resolve和reject方法以主動觸發狀态轉變和通知觀察者。

參考文獻

深入了解 Promise

JavaScript Promises ... In Wicked Detail

本文來自網易雲社群,經作者顧靜授權釋出。