深刻理解 Promise 實現細節

在以前的異步JavaScript與Promise一文中,我介紹了Promise以及它在異步JavaScript中的使用意義。通常來講,咱們是經過各類JavaScript庫來應用Promise的。隨着使用Promise的機會愈來愈多,你也可能像我這樣會關心Promise究竟是如何工做的。顯然,瞭解Promise的實現細節,能夠幫助咱們更好地應用它。尤爲是碰到一些Promise的問題時,也許能夠更快速、更準確地定位緣由,並解決它。segmentfault

很是慶幸,在[Promises/A wiki][]中位於庫列表第一位的[Q][],提供了它做爲一個Promise庫的[基本設計原理解析][]。本文將主要根據Q的這篇文章,探討Promise的實現細節。數組

Promise核心說明

儘管Promise已經有本身的規範,但目前的各種Promise庫,在Promise的實現細節上是有差別的,部分API甚至在乎義上徹底不一樣。但Promise的核心內容,是相通的,它就是then方法。在相關術語中,promise指的就是一個有then方法,且該方法能觸發特定行爲的對象或函數。promise

有關Promise核心說明的細節,推薦閱讀[Promises/A+][]。這篇文章是寫給Promise庫的開發者的,你能夠找到各類對Promise特性的說明。[Promises/A+][]但願開發者聽從這些特性,以實現能夠共同使用的Promise(也就是說,不一樣的Promise庫也可共用)。app

Promise能夠有不一樣的實現方式,所以Promise核心說明並不會討論任何具體的實現代碼。異步

先閱讀Promise核心說明的意思是:看,這就是須要寫出來的結果,請參照這個結果想想怎麼用代碼寫出來吧。ide

起步:用這一種方式理解Promise

回想一下Promise解決的是什麼問題?回調。例如,函數doMission1()表明第一件事情,如今,咱們想要在這件事情完成後,再作下一件事情doMission2(),應該怎麼作呢?函數

先看看咱們常見的回調模式。doMission1()說:「你要這麼作的話,就把doMission2()交給我,我在結束後幫你調用。」因此會是:工具

doMission1(doMission2);

Promise模式又是如何呢?你對doMission1()說:「不行,控制權要在我這裏。你應該改變一下,你先返回一個特別的東西給我,而後我來用這個東西安排下一件事。」這個特別的東西就是Promise,這會變成這樣:post

doMission1().then(doMission2);

能夠看出,Promise將回調模式的主從關係調換了一個位置(翻身作主人!),多個事件的流程關係,就能夠這樣集中到主幹道上(而不是分散在各個事件函數以內)。設計

好了,如何作這樣一個轉換呢?從最簡單的狀況來吧,假定doMission1()的代碼是:

function doMission1(callback){
    var value = 1;
    callback(value);
}

那麼,它能夠改變一下,變成這樣:

function doMission1(){
    return {
        then: function(callback){
            var value = 1;
            callback(value);
        }
    };
}

這就完成了轉換。雖然並非實際有用的轉換,但到這裏,其實已經觸及了Promise最爲重要的實現要點,即Promise將返回值轉換爲帶then方法的對象

進階:Q的設計路程

從defer開始

design/q0.js是Q初步成型的第一步。它建立了一個名爲defer的工具函數,用於建立Promise:

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

這段源碼能夠看出,運行defer()將獲得一個對象,該對象包含resolvethen兩個方法。請回想一下jQuery的Deferred(一樣有resolvethen),這兩個方法將會是近似的效果。then會參考pending的狀態,若是是等待狀態則將回調保存(push),不然當即調用回調。resolve則將確定這個Promise,更新值的同時運行完全部保存的回調。defer的使用示例以下:

var oneOneSecondLater = function () {
    var result = defer();
    setTimeout(function () {
        result.resolve(1);
    }, 1000);
    return result;
};

oneOneSecondLater().then(callback);

這裏oneOneSecondLater()包含異步內容(setTimeout),但這裏讓它當即返回了一個defer()生成的對象,而後將對象的resolve方法放在異步結束的位置調用(並附帶上值,或者說結果)。

到此,以上代碼存在一個問題:resolve能夠被執行屢次。所以,resolve中應該加入對狀態的判斷,保證resolve只有一次有效。這就是Q下一步的design/q1.js(僅差別部分):

resolve: function (_value) {
    if (pending) {
        value = _value;
        for (var i = 0, ii = pending.length; i < ii; i++) {
            var callback = pending[i];
            callback(value);
        }
        pending = undefined;
    } else {
        throw new Error("A promise can only be resolved once.");
    }
}

對第二次及更多的調用,能夠這樣拋出一個錯誤,也能夠直接忽略掉。

分離defer和promise

在前面的實現中,defer生成的對象同時擁有then方法和resolve方法。按照定義,promise關心的是then方法,至於觸發promise改變狀態的resolve,是另外一回事。因此,Q接下來將擁有then方法的promise,和擁有resolve的defer分離開來,各自獨立使用。這樣就好像劃清了各自的職責,各自只留必定的權限,這會使代碼邏輯更明晰,易於調整。請看design/q3.js:(q2在此跳過)

var isPromise = function (value) {
    return value && typeof value.then === "function";
};

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = _value;
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    callback(value);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (callback) {
                if (pending) {
                    pending.push(callback);
                } else {
                    callback(value);
                }
            }
        }
    };
};

若是你仔細對比一下q1,你會發現區別很小。一方面,再也不拋出錯誤(改成直接忽略第二次及更多的resolve),另外一方面,將then方法移動到一個名爲promise的對象內。到這裏,運行defer()獲得的對象(就稱爲defer吧),將擁有resolve方法,和一個promise屬性指向另外一個對象。這另外一個對象就是僅有then方法的promise。這就完成了分離。

前面還有一個isPromise()函數,它經過是否有then方法來判斷對象是不是promise(duck-typing的判斷方法)。爲了正確使用和處理分離開的promise,會像這樣須要將promise和其餘值區分開來。

實現promise的級聯

接下來會是至關重要的一步。到前面到q3爲止,所實現的promise都是不能級聯的。但你所熟知的promise應該支持這樣的語法:

promise.then(step1).then(step2);

以上過程能夠理解爲,promise將能夠創造新的promise,且取自舊的promise的值(前面代碼中的value)。要實現then的級聯,須要作到一些事情:

  • then方法必須返回promise。

  • 這個返回的promise必須用傳遞給then方法的回調運行後的返回結果,來設置本身的值。

  • 傳遞給then方法的回調,必須返回一個promise或值。

design/q4.js中,爲了實現這一點,新增了一個工具函數ref

var ref = function (value) {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            return ref(callback(value));
        }
    };
};

這是在着手處理與promise關聯的value。這個工具函數將對任一個value值作一次包裝,若是是一個promise,則什麼也不作,若是不是promise,則將它包裝成一個promise。注意這裏有一個遞歸,它確保包裝成的promise可使用then方法級聯。爲了幫助理解它,下面是一個使用的例子:

ref("step1").then(function(value){
    console.log(value); // "step1"
    return 15;
}).then(function(value){
    console.log(value); // 15
});

你能夠看到value是怎樣傳遞的,promise級聯須要作到的也是如此。

design/q4.js經過結合使用這個ref函數,將原來的defer轉變爲可級聯的形式:

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value); // values wrapped in a promise
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    value.then(callback); // then called instead
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback) {
                var result = defer();
                // callback is wrapped so that its return
                // value is captured and used to resolve the promise
                // that "then" returns
                var callback = function (value) {
                    result.resolve(_callback(value));
                };
                if (pending) {
                    pending.push(callback);
                } else {
                    value.then(callback);
                }
                return result.promise;
            }
        }
    };
};

原來callback(value)的形式,都修改成value.then(callback)。這個修改後效果其實和原來相同,只是由於value變成了promise包裝的類型,會須要這樣調用。

then方法有了較多變更,會先新生成一個defer,並在結尾處返回這個defer的promise。請注意,callback再也不是直接取用傳遞給then的那個,而是在此基礎之上增長一層,並把新生成的defer的resolve方法放置在此。此處能夠理解爲,then方法將返回一個新生成的promise,所以須要把promise的resolve也預留好,在舊的promise的resolve運行後,新的promise的resolve也會隨之運行。這樣才能像管道同樣,讓事件按照then鏈接的內容,一層一層傳遞下去。

加入錯誤處理

promise的then方法應該能夠包含兩個參數,分別是確定和否認狀態的處理函數(onFulfilledonRejected)。前面咱們實現的promise還只能轉變爲確定狀態,因此,接下來應該加入否認狀態部分。

請注意,promise的then方法的兩個參數,都是可選參數。design/q6.jsq5也跳過)加入了工具函數reject來幫助實現promise的否認狀態:

var reject = function (reason) {
    return {
        then: function (callback, errback) {
            return ref(errback(reason));
        }
    };
};

它和ref的主要區別是,它返回的對象的then方法,只會取第二個參數的errback來運行。design/q6.js的其他部分是:

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value);
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    value.then.apply(value, pending[i]);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback, _errback) {
                var result = defer();
                // provide default callbacks and errbacks
                _callback = _callback || function (value) {
                    // by default, forward fulfillment
                    return value;
                };
                _errback = _errback || function (reason) {
                    // by default, forward rejection
                    return reject(reason);
                };
                var callback = function (value) {
                    result.resolve(_callback(value));
                };
                var errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    value.then(callback, errback);
                }
                return result.promise;
            }
        }
    };
};

這裏的主要改動是,將數組pending只保存單個回調的形式,改成同時保存確定和否認的兩種回調的形式。並且,在then中定義了默認的確定和否認回調,使得then方法知足了promise的2個可選參數的要求。

你也許注意到defer中仍是隻有一個resolve方法,而沒有相似jQuery的reject。那麼,錯誤處理要如何觸發呢?請看這個例子:

var defer1 = defer(),
promise1 = defer1.promise;
promise1.then(function(value){
    console.log("1: value = ",  value);
    return reject("error happens"); 
}).then(function(value){
    console.log("2: value = ", value);
}).then(null, function(reason){
    console.log("3: reason = ", reason);
});
defer1.resolve(10);

// Result:
// 1: value = 10
// 3: reason = error happens

能夠看出,每個傳遞給then方法的返回值是很重要的,它將決定下一個then方法的調用結果。而若是像上面這樣返回工具函數reject生成的對象,就會觸發錯誤處理。

融入異步

終於到了最後的design/q7.js。直到前面的q6,還存在一個問題,就是then方法運行的時候,多是同步的,也多是異步的,這取決於傳遞給then的函數(例如直接返回一個值,就是同步,返回一個其餘的promise,就能夠是異步)。這種不肯定性可能帶來潛在的問題。所以,Q的後面這一步,是確保將全部then轉變爲異步。

design/q7.js定義了另外一個工具函數enqueue

var enqueue = function (callback) {
    //process.nextTick(callback); // NodeJS
    setTimeout(callback, 1); // Naïve browser solution
};

顯然,這個工具函數會將任意函數推遲到下一個事件隊列運行。

design/q7.js其餘的修改點是(只顯示修改部分):

var ref = function (value) {
    // ...
    return {
        then: function (callback) {
            var result = defer();
            // XXX
            enqueue(function () {
                result.resolve(callback(value));
            });
            return result.promise;
        }
    };
};

var reject = function (reason) {
    return {
        then: function (callback, errback) {
            var result = defer();
            // XXX
            enqueue(function () {
                result.resolve(errback(reason));
            });
            return result.promise;
        }
    };
};

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            // ...
                    enqueue(function () {
                        value.then.apply(value, pending[i]);
                    });
            // ...
        },
        promise: {
            then: function (_callback, _errback) {
                    // ...
                    enqueue(function () {
                        value.then(callback, errback);
                    });
                    // ...
            }
        }
    };
};

即把原來的value.then的部分,都轉變爲異步。

到此,Q提供的Promise設計原理q0~q7,所有結束。

結語

即使本文已是這麼長的篇幅,但所講述的也只到基礎的Promise。大部分Promise庫會有更多的API來應對更多和Promise有關的需求,例如all()spread(),不過,讀到這裏,你已經瞭解了實現Promise的核心理念,這必定對你從此應用Promise有所幫助。

在我看來,Promise是精巧的設計,我花了至關一些時間才差很少理解它。Q做爲一個典型Promise庫,在思路上走得很明確。能夠感覺到,再複雜的庫也是先從基本的要點開始的,若是咱們本身要作相似的事,也應該保持這樣的心態一點一點進步。

(從新編輯自個人博客,原文地址:http://acgtofe.com/posts/2015...

相關文章
相關標籤/搜索