文章出處

一、前言                            

  avalon.js的影響力愈發強勁,而作為子模塊之一的mmDeferred必然成為異步調用模式學習之旅的又一站呢!本文將記錄我對mmDeferred的認識,若有紕漏請各位指正,謝謝。項目請見:mmDeferred@github

 

二、API說明                          

   {Deferred} Deferred({Function|Object} mixin?) ,創建一個Deferred實例,當mixin的類型為Object時,將mixin的屬性和函數附加到Deferred實例的Promise實例上。

    {String} state() ,獲取當前Deferred實例的狀態,含三種狀態:pending,fulfilled,rejected;轉換關系為:pending->fulfilled,pending-rejected。

   {Promise} then({Function} resolvefn?, {Function} rejectfn?, {Function} notifyfn?, {Function} ensurefn?) ,向當前的Deferred實例添加四類回調函數,并返回一個新的Promise實例。其中resolvefn是實例狀態轉換為fulfilled時調用,而rejectfn是實例狀態轉換為rejected時調用,而notifyfn則相當于Promises/A+規范中的progressHandler一樣與實例狀態無關只需調用notify函數則調用notifyfn,ensurefn的作用為模擬當前Deferred實例執行resolvefn、rejectfn和notifyfn的finally語句塊,無論執行前面三個函數的哪一個均會執行ensurefn。

    {Promise} otherwise({Function} rejectfn?) ,向當前的Deferred實例添加rejectfn回調函數,并返回一個新的Promise實例。

    {Promise} ensure({Function} ensurefn?) ,向當前的Deferred實例添加ensurefn回調函數,并返回一個新的Promise實例。

    {undefined} resolve(...[*]) ,用于觸發fulfill回調——也就是觸發調用當前Deferred實例的resolvefn函數的請求,僅能調用一次。

    {undefined} reject(...[*]) ,用于觸發reject回調——也就是觸發調用當前Deferred實例的rejectfn函數的請求,僅能調用一次。

    {undefined} notify(...[*]) ,用于觸發notify回調——也就是觸發調用當前Deferred實例的notifyfn函數的請求,能調用多次。

   {Promise} Deferred.all(...[Promise]) ,要求傳入多個Promise對象,當它們都正常觸發時,就執行它的resolve回調。相當于jQuery的when方法,但all更標準,是社區公認的函數。

     {Promise} Deferred.any(...[Promise]) ,要求傳入多個Promise對象,最先正常觸發的Promise對象,將執行它的resolve回調。

 

、源碼剖析                              

  首先要了解的是mmDeferred中存在Deferred和Promise兩個操作集合(兩者操作同一個的數據結構實例),Promise用于向實例添加四類回調函數,而Deferred用于發起實例狀態變化或觸發回調函數調用的操作,并且限制為僅通過Deferred函數返回的為Deferred操作集合,而其他API返回的均為Promise操作集合。

  另外,值得注意的有以下幾點

  1. mmDeferred在實例狀態轉換的實現方式上是采取先調用回調函數再修改實例狀態的方式;

  2. resolve、reject等的實現上并不是統一采用異步調用的方式在執行回調函數,而是當實例已經被添加了回調函數時同步執行回調函數,當未添加回調函數時則發起異步調用,讓當前執行的代碼塊有機會向實例添加回調函數;

  3. 不支持以下方式的回調函數晚綁定:

var deferred = Deferred()
deferred.resolve()
setTimeout(function(){
  deferred.promise.then(function(){
    console.log('hello world')
  })
}, 0)

  在代碼結構上值得注意的是

  1. 利用JS中變量聲明自動提升(hoist)的特性,通過前置return語句將對外接口與具體實現的代碼分離。

  2. 提取resolve、reject等函數的共性到私有函數_fire中,提供then、otherwise等函數的共性到私有函數_post中,提供Deferred.all和Deferred.any的共性到私有函數some中,避免重復代碼從而大大減少代碼量。

  待改進點我覺得應該將_fire和_post函數移出至Deferred函數之外,通過入參取代閉包引用外部變量的方式來獲取和修改實例屬性,那么每次調用Deferred函數時就不會重新聲明新的_fire和_post函數了。

  存在疑惑的地方為

    假設當前實例A狀態為pending,那么執行notify回調函數后當前實例A的狀態是不變的,當后續執行的ensure函數拋出異常,那么將調用鏈表中下一個實例B的reject方法導致實例B的狀態為rejected,但實例A狀態依然為pending。這時再次調用實例B的resolve或reject方法均不會觸發執行相應的回調函數,但可通過調用實例A的resovle或reject方法執行實例A和實例B相應的回調函數。

  下面是源碼

define("mmDeferred", ["avalon"], function(avalon) {
    var noop = function() {
    }
    function Deferred(mixin) {
        var state = "pending"
            // 標識是否已經添加了回調函數
            , dirty = false
        function ok(x) {
            state = "fulfilled"
            return x
        }
        function ng(e) {
            state = "rejected"
            // 將異常往后傳遞
            throw e
        }
        // Deferred實例
        var dfd = {
            callback: {
                resolve: ok,
                reject: ng,
                notify: noop,
                ensure: noop
            },
            dirty: function() {
                return dirty
            },
            state: function() {
                return state
            },
            promise: {
                then: function() {
                    return _post.apply(null, arguments)
                },
                otherwise: function(onReject) {
                    return _post(0, onReject)
                },
                ensure: function(onEnsure) {
                    return _post(0, 0, 0, onEnsure)
                },
                _next: null
            }
        }
        if (typeof mixin === "function") {
            mixin(dfd.promise)
        } else if (mixin && typeof mixin === "object") {
            for (var i in mixin) {
                if (!dfd.promise[i]) {
                    dfd.promise[i] = mixin[i]
                }
            }
        }

        "resolve,reject,notify".replace(/\w+/g, function(method) {
            dfd[method] = function() {
                var that = this, args = arguments
                if (that.dirty()) {
                    // 若已經添加了回調函數,則馬上同步調用
                    _fire.call(that, method, args)
                } else {
                    // 若未添加回調函數,則發起異步調用,讓當前代碼塊的后續部分有足夠的時間添加回調函數
                    Deferred.nextTick(function() {
                        _fire.call(that, method, args)
                    })
                }
            }
        })

        return dfd

        /** 精彩之處:
         * 由于JS會將變量聲明自動提升(hoist)到代碼塊的頭部
         * 因此這里將私有方法寫在return語句之后從而更好地格式化代碼結構
         */

        // 添加回調函數到當前Deferred實例上
        function _post() {
            var index = -1, fns = arguments;
            "resolve,reject,notify,ensure".replace(/\w+/g, function(method) {
                var fn = fns[++index];
                if (typeof fn === "function") {
                    dirty = true
                    if (method === "resolve" || method === "reject") {
                        // 將修改Deferred實例狀態的功能封裝到回調函數中
                        // 也就是先調用回到函數再修改實例狀態
                        dfd.callback[method] = function() {
                            try {
                                var value = fn.apply(this, arguments)
                                state = "fulfilled"
                                return value
                            } catch (err) {
                                state = "rejected"
                                return err
                            }
                        }
                    } else {
                        dfd.callback[method] = fn;
                    }
                }
            })
            // 創建鏈表的下一個Deferred實例
            var deferred = dfd.promise._next = Deferred(mixin)
            return deferred.promise;
        }

        function _fire(method, array) {
            var next = "resolve", value
            if (this.state() === "pending" || method === "notify") {
                var fn = this.callback[method]
                try {
                    value = fn.apply(this, array);
                } catch (e) {//處理notify的異常
                    value = e
                }
                if (this.state() === "rejected") {
                    next = "reject"
                } else if (method === "notify") {
                    next = "notify"
                }
                array = [value]
            }
            var ensure = this.callback.ensure
            if (noop !== ensure) {
                try {
                    ensure.call(this)//模擬finally
                } catch (e) {
                    next = "reject";
                    array = [e];
                }
            }
            var nextDeferred = this.promise._next
            if (Deferred.isPromise(value)) {
                // 如果回調函數返回值為Deferred實例,那么就將該實例插入nextDeferred之前
                value._next = nextDeferred
            } else {
                if (nextDeferred) {
                    _fire.call(nextDeferred, next, array);
                }
            }
        }
    }

    window.Deferred = Deferred;
    Deferred.isPromise = function(obj) {
        return !!(obj && typeof obj.then === "function");
    };

    function some(any, promises) {
        var deferred = Deferred(), n = 0, result = [], end
        function loop(promise, index) {
            promise.then(function(ret) {
                if (!end) {
                    result[index] = ret//保證回調的順序
                    n++;
                    if (any || n >= promises.length) {
                        deferred.resolve(any ? ret : result);
                        end = true
                    }
                }
            }, function(e) {
                end = true
                deferred.reject(e);
            })
        }
        for (var i = 0, l = promises.length; i < l; i++) {
            loop(promises[i], i)
        }
        return deferred.promise;
    }
    Deferred.all = function() {
        return some(false, arguments)
    }
    Deferred.any = function() {
        return some(true, arguments)
    }
    Deferred.nextTick = avalon.nextTick
    return Deferred
})

 

四、總結                            

  源碼中還提供了相關資料的鏈接,可以讓我們更了解Promise/A+規范哦!

  尊重原創,轉載請注明來自:http://www.cnblogs.com/fsjohnhuang/p/4162646.html 肥子John

 

五、參考                            

《JavaScript框架設計》


文章列表




Avast logo

Avast 防毒軟體已檢查此封電子郵件的病毒。
www.avast.com


arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()