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

Norma1491 7年前發布 | 24K 次閱讀 Promise JavaScript開發

前言

前一陣子記錄了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) { //一些處理 })</code></pre>

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

}</code></pre>

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

  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 轉化而來,兩者之間不能互相轉換。一圖勝千言:

改進后的代碼是這樣的:

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

}</code></pre>

上述代碼的思路是這樣的: 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); }); }); }</code></pre>

這種場景相信用過 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.onResolved) {
        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);

}</code></pre>

我們結合 例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); }); }); }</code></pre>

  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 中的回調。

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

失敗處理

在異步操作失敗時,標記其狀態為 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) })</code></pre>

有了之前處理 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);

}</code></pre>

上述代碼增加了新的 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);
} 

}</code></pre>

如果在異步操作中,多次執行 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

來自:https://mengera88.github.io/2017/05/18/Promise%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

 

 本文由用戶 Norma1491 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!