JS魔法堂:mmDeferred源码剖析

一、前言                            

  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+规范哦!

五、参考                            

《JavaScript框架设计》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏信安之路

利用Java反射和类加载机制绕过JSP后门检测

JSP 后门,一般是指文件名以 .jsp 等后缀结尾的,可运行于 Java servlet 及相关容器和组件内的通用 JSP 脚本。

40100
来自专栏JavaQ

多参数方法进阶

很多高级工程师还在写包含N个参数的方法、使用setter方法构造实例,其实这些方式都是过时并且有很大缺陷的,本篇将深入讲解这些问题及解决方法。 多参数方法的问题...

359110
来自专栏java一日一条

代码优化那些事

/**************************************************************************/

7330
来自专栏顶级程序员

全面深入理解Python面向对象编程

面向过程编程最易被初学者接受,其往往用一长段代码来实现指定功能,开发过程中最常见的操作就是粘贴复制,即:将之前实现的代码块复制到现需功能处。

31750
来自专栏Android 研究

Android JNI学习(三)——Java与Native相互调用

前面两篇文章简单的介绍了JNI,下面我们就进一步了解下一下JNI的调用原则,要想了解JNI的调用原则, 前面我们说了JNI中的JNIEnv以及Java类型和na...

28230
来自专栏轮子工厂

5. 很“迷”的字符与字符串

最近一直在为自己的浏览量而担忧啦,都快被厂长大人约谈了……我真的有尽力在写稿子哦,所以也请各位老铁,如果觉得我的文章还不错就转发到朋友圈或者微信群之类的,让更多...

14820
来自专栏IT派

全面深入理解Python面向对象编程

面向过程编程最易被初学者接受,其往往用一长段代码来实现指定功能,开发过程中最常见的操作就是粘贴复制,即:将之前实现的代码块复制到现需功能处。

15120
来自专栏小勇DW3

java设计模式之模板模式以及钩子方法使用

  模板方法模式是通过把不变行为搬到超类,去除子类里面的重复代码提现它的优势,它提供了一个很好的代码复用平台。当不可变和可变的方法在子类中混合在一起的时候,

28440
来自专栏编程一生

PHP开发人员对JAVA的WEB开发入门(初版-基础知识)

13840
来自专栏博客园

MSIL学习------从HelloWorld开始

  前段时间突然想搞搞IL语言,于是在博客园中找到了包建强前辈关于IL的文章学习,并且在包前辈博客里看到了09年他与赵劼前辈关于是否有必要学习IL语言的争论,作...

15130

扫码关注云+社区

领取腾讯云代金券