JS 魔法堂:jsDeferred 源码剖析

705 阅读31分钟
原文链接: www.cnblogs.com

一、前言                            

   最近在研究Promises/A+规范及实现,而Promise/A+规范的制定则很大程度地参考了由日本geek cho45发起的jsDeferred项目(《JavaScript框架设计》提供该资讯,再次感谢),追本溯源地了解jsDeferred是十分有必要的,并且当你看过官网(http://cho45.stfuawsc.com/jsdeferred/)的新手引导后就会有种不好好学学就太可惜的感觉了,而只看API和使用指南是无法满足我对它的好奇心的,通过解读源码读透它的设计思想才是根本。

  本文部分内容将和《JS魔法堂:剖析源码理解Promises/A》中的内容作对比来讲解。

  由于内容较多,特设目录一坨

    二、jsDeferred与Promises/A的核心区别

    三、从API说起

  四、细说功能实现

  1. 基础功能部分

    1.1. 构造函数  

    1.2. call函数

    1.3. fail函数

    1.4. next函数

    1.5. error函数

  2. 辅助功能部分

    2.1. Deferred.define函数

    2.2. Deferred.isDeferred函数

    2.3. Deferred.wait函数

    2.4. Deferred.next函数

    2.5. Deferred.call函数

    2.6. Deferred.loop函数

    2.7. Deferred.parallel函数

    2.8. Deferred.earlier函数

    2.9. Deferred.chain函数

    2.10. Deferred.connect函数

    2.11. Deferred.register函数

    2.12. Deferred.retry函数

    2.13. Deferred.repeat函数

  五、总结

    六、参考

二、jsDeferred与Promises/A的核心区别              

  jsDeferred的特点

  ①. 内部通过单向链表结果存储 成功事件处理函数、失败事件处理函数 和 链表中下一个Deferred类型对象;

  ②. Deferred实例内部没有状态标识(也就是说Deferred实例没有自定义的生命周期);

  ③. 由于Deferred实例没有状态标识,因此不支持成功/失败事件处理函数的晚绑定;

  ④. Deferred实例的成功/失败事件是基于事件本身的触发而被调用的;

  ⑤. 由于Deferred实例没有状态标识,因此成功/失败事件可被多次触发,也不存在不变值作为事件处理函数入参的说法;

  Promises/A的特点:

  ①. 内部通过单向链表结果存储 成功事件处理函数、失败事件处理函数 和 链表中下一个Promise类型对象;

  ②. Promise实例内部有状态标识:pending(初始状态)、fulfilled(成功状态)和rejected(失败状态),且状态为单方向移动“pending->fulfilled","pending->rejected";(也就是Promse实例存在自定义的生命周期,而生命周期的每个阶段具备不同的事件和操作)

  ③. 由于Promise实例含状态标识,因此支持事件处理函数的晚绑定;

  ④. Promise实例的成功/失败事件函数是基于Promise的状态而被调用的。

  核心区别

      Promises调用成功/失败事件处理函数的两种流程

         ①. 调用resolve/reject方法尝试改变Promise实例的状态,若成功改变其状态,则调用Promise当前状态相应的事件处理函数;(类似于触发onchange事件)

         ②. 通过then方法进行事件绑定,若Promise实例的状态不是pending,则调用Promise当前状态相应的事件处理函数。

         由上述可以知道Promises的成功/失败事件处理函数均基于Promise实例的状态而被调用,而非成功/失败事件。

      jsDeferred调用成功/失败事件处理函数的流程

         ①. 调用call/fail方法触发成功/失败事件,则调用相应的事件处理函数。

         因此jsDeferred的是基于事件的。

三、从API说起                            

  下列内容均为大概介绍API接口,具体用法请参考官网。

  1. 构造函数

      Deferred ,可通过 new Deferred() 或 Deferred() 两种方式创建Deferred实例。

  2. 实例方法

   Deferred next({Function} fn) ,绑定成功事件处理函数,返回一个新的Deferred实例。

       Deferred error({Function} fn) ,绑定失败事件处理函数,返回一个新的Deferred实例。

       Deferred call(val*) ,触发成功事件,返回一个新的Deferred实例。

   Deferred fail(val*) ,触发失败事件,返回一个新的Deferred实例。

  3. 静态属性

       {Function} Deferred.ok ,默认的成功事件处理函数。

   {Function} Deferred.ng ,默认的失败事件处理函数。

       {Array} Deferred.methods ,默认的向外暴露的静态方法。(供 Deferred.define方法 使用)

  4. 静态方法

       {Function}Deferred Deferred.define(obj, list) ,暴露list制定的静态方法到obj上,obj默认是全局对象。

       Deferred Deferred.call({Function} fn [, arg]*) ,创建一个Deferred实例并且触发其成功事件。

       Deferred Deferred.next({Function} fn) ,创建一个Deferred实例并且触发其成功事件,其实就是无法传入参到成功事件处理函数的 Deferred.call() 。

       Deferred Deferred.wait(sec) ,创建一个Deferred实例并且等sec秒后触发其成功事件。

   Deferred Deferred.loop(n, fun) ,循环执行fun并且上一个fun,最后一个fun的返回值将作为Deferred实例的成功事件处理函数的入参。

       Deferred Deferred.parallel(dl) ,将dl中非Deferred对象转换为Deferred对象,然后并行触发dl中的Deferred实例的成功事件,当

所有Deferred对象均调用了成功事件处理函数后,返回的Deferred实例则触发成功事件,并且所有返回值将被封装为数组作为Deferred实例的成功事件处理函数的入参。

       Deferred Deferred.earlier(dl) ,将dl中非Deferred对象转换为Deferred对象,然后并行触发dl中的Deferred实例的成功事件,当

其中一个Deferred对象调用了成功事件处理函数则终止其他Deferred对象的触发成功事件,而返回的Deferred实例则触发成功事件,并且那个被调用的成功事件处理函数的返回值为Deferred实例的成功事件处理函数的入参。

       Boolean Deferred.isDeferred(obj) ,判断obj是否为Deferred类型。

   Deferred Deferred.chain(args) ,创建一个Deferred实例一次执行args的函数

   Deferred Deferred.connect(funo, options) ,将某个函数封装为Deferred对象

   Deferred Deferred.register(name, fn) ,将静态方法附加到Deferred.prototype上

   Deferred Deferred.retry(retryCount, funcDeferred, options) ,尝试调用funcDeffered方法(返回值类型为Deferred)retryCount,直到触发成功事件或超过尝试次数为止。

   Deferred Deferred.repeat(n, fun) ,循环执行fun方法n次,若fun的执行事件超过20毫秒则先将UI线程的控制权交出,等一会儿再执行下一轮的循环。

   jsDeferred采用DSL风格的API设计,语义化我喜欢啊!

四、细说功能实现                          

  1. 基础功能部分

    1.1. 构造函数

function Deferred () { return (this instanceof Deferred) ? this.init() : new Deferred() }
// 默认的成功事件处理函数
Deferred.ok = function (x) { return x };
// 默认的失败事件处理函数
Deferred.ng = function (x) { throw  x };
Deferred.prototype = {
    // 初始化函数
    init : function () {
    this._next    = null;
    this.callback = {
        ok: Deferred.ok,
        ng: Deferred.ng
    };
    return this;
}};

    1.2. call函数

Deferred.prototype.call = function (val) { return this._fire("ok", val) };
Deferred.prototype._filre = function(okng, value){
  var next = "ok";
  try {
    // 调用当前Deferred实例的事件处理函数
    value = this.callback[okng].call(this, value);
  } catch (e) {
    next = "ng";
    value = e;
    if (Deferred.onerror) Deferred.onerror(e);
  }
  if (Deferred.isDeferred(value)) {
    // 若事件处理函数返回一个新Deferred实例,则将新Deferred实例的链表指针指向当前Deferred实例的链表指针指向,
    // 这样新Deferred实例的事件处理函数就会先与原链表中其他Deferred实例的事件处理函数被调用。
    value._next = this._next;
  } else {
    if (this._next) this._next._fire(next, value);
  }
  return this;
};

    1.3. fail函数

Deferred.prototype.fail = function (err) { return this._fire("ng", err) };

    1.4. next函数

Deferred.prototype.next = function (fun) { return this._post("ok", fun) };
Deferred.prototype._post = function (okng, fun) {
    // 创建一个新的Deferred实例,插入Deferred链表尾,并将事件处理函数绑定到新的Deferred上  
    this._next = new Deferred();
    this._next.callback[okng] = fun;
    return this._next;
};

    《JS魔法堂:剖析源码理解Promises/A》中的官网实现示例是将事件处理函数绑定到当前的Promise实例,而不是新创的Promise实例。而jsDeferred则是绑定到新创建的Deferred实例上。这是因为Promise实例默认的事件处理函数为undefined,而Deferred是含默认的事件处理函数的。

    1.5. error函数

Deferred.prototype.error = function (fun) { return this._post("ng", fun) };

  2. 辅助功能部分

  jsDeferred的基础功能部分都十分好理解,我认为它的精彩之处在于类方法——辅助功能部分。

    2.1. Deferred.define函数实现

Deferred.define = function (obj, list) {
    if (!list) list = Deferred.methods;
    // 以全局对象作为默认的入潜目标
    // 由于带代码运行在sloppy模式,因此函数内的this指针指向全局对象。若运行在strict模式,则this指针值为undefined。
    // 即使被以strict模式运行的程序调用,本段程序依然以sloppy模式运行使用
    if (!obj) obj = (function getGlobal () { return this })();
    for (var i = 0; i < list.length; i++) {
    var n = list[i];
        obj[n] = Deferred[n];
    }
    return Deferred;
};

    当我第一次看新手引导中的示例代码

Deferred.define();
next(function(){
  ............
}).next(function(){
  ...............
});

    这不是就和jdk1.5的静态导入 import static一样吗?!两者同样是以入侵的方式将类方法附加到当前执行上下文中,这种导入的方式有人喜欢有人明令禁止(原上下文被破坏,维护性大大降低)。而我则有一个准则,就是导入的类方法足够少(5个左右,反正能看一眼API就记得那种),团队的小伙伴们均熟知这些API,并且仅以此方式导入一个类的方法到当前执行上下文中。其实能满足这些要求的库不多,还不如起个短小精干的类名作常规导入更实际。这里扯远了,我再看看 Deferred.define方法 吧,其实它除了将类方法导入到当前执行上下文,还可以导入到一个指定的对象中(这个方法比较中用!)

var ctx = {};
Deferred.define(ctx);
ctx.next(function(){
   ..............
}).next(function(){
   .............
});

    2.2. Deferred.isDeferred函数实现

Deferred.isDeferred = function (obj) {
    return !!(obj && obj._id === Deferred.prototype._id);
};
// 貌似是Mozilla有个插件也叫Deferred,因此不能通过instanceof来检测。cho45于是自定义标志位来作检测,并在github上提交fxxking Mozilla,哈哈!
Deferred.prototype._id = 0xe38286e381ae;

    2.3. Deferred.wait函数实现

Deferred.wait = function (n) {
    var d = new Deferred(), t = new Date();
    var id = setTimeout(function () {
        // 入参为实际等待毫秒数,由于各浏览器的setTimeout均有一个最小精准度参数(IE9+和现代浏览器为4msec,IE5.5~8为15.4msec),因此实际等待的时间一定被希望的长
        d.call((new Date()).getTime() - t.getTime());
    }, n * 1000);
    d.canceller = function () { clearTimeout(id) };
    return d;
}; 

    刚看到该函数时我确实有点小鸡冻,我们可以将《JS魔法堂:剖析源码理解Promises/A》的第三节“从感性领悟”下的示例,写得于现实生活的思路更贴近了。

// 任务定义部分
var 下班 = function(){};
var 搭车 = function(){};
var 接小孩 = function(){};
var 回家 = function(){};

// 流程部分
next(下班)
    .wait(10*60)
    .next(下班)
    .wait(10*60) 
    .next(搭车)
    .wait(10*60) 
    .next(接小孩)
    .wait(20*60)
    .next(回家);

    2.4. Deferred.next函数实现

      该函数可为是真个jsDeferred最出彩的地方了,也是后续其他方法的实现基础,它的功能是创建一个新的Deferred对象,并且异步执行该Deferred对象的call方法来触发成功事件。针对运行环境的不同,它提供了相应的异步调用的实现方式并作出降级处理。

Deferred.next = 
    Deferred.next_faster_way_readystatechange ||
    Deferred.next_faster_way_Image ||
    Deferred.next_tick ||
    Deferred.next_default;

      由浅入深,我们先看看使用setTimeout实现异步的 Deferred.next_default方法 (存在最小时间精度的问题)

Deferred.next_default = function (fun) {
    var d = new Deferred();
    var id = setTimeout(function () { d.call() }, 0);
    d.canceller = function () { clearTimeout(id) };
    if (fun) d.callback.ok = fun;
    return d;
};

      然后是针对nodejs的 Deferred.next_tick方法 

Deferred.next_tick = function (fun) {
    var d = new Deferred();
    // 使用process.nextTick来实现异步调用 
    process.nextTick(function() { d.call() });
    if (fun) d.callback.ok = fun;
    return d;
};

      然后就是针对现代浏览器的 Deferred.next_faster_way_Image方法 

Deferred.next_faster_way_Image = function (fun) {
    var d = new Deferred();
    var img = new Image();
    var handler = function () {
        d.canceller();
        d.call();
    };
    img.addEventListener("load", handler, false);
    img.addEventListener("error", handler, false);
    d.canceller = function () {
        img.removeEventListener("load", handler, false);
        img.removeEventListener("error", handler, false);
    };
    // 请求一个无效data uri scheme导致马上触发load或error事件
    // 注意:先绑定事件处理函数,再设置图片的src是个良好的习惯。因为设置img.src属性后就会马上发起请求,假如读的是缓存那有可能还未绑定事件处理函数,事件已经被触发了。
    img.src = "data:image/png," + Math.random();
    if (fun) d.callback.ok = fun;
    return d;
};

      最后就是针对IE5.5~8的 Deferred.next_faster_way_readystatechange方法 

Deferred.next_faster_way_readystatechange = ((typeof window === 'object') && (location.protocol == "http:") && !window.opera && /\bMSIE\b/.test(navigator.userAgent)) && function (fun) {
    var d = new Deferred();
    var t = new Date().getTime();
    /* 原理:
            由于浏览器对并发请求数作出限制(IE5.5~8为2~3,IE9+和现代浏览器为6),
             因此当并发请求数大于上限时,会让请求的发起操作排队执行,导致延时更严重了。
       实现手段:
            以150毫秒为一个周期,每个周期以通过setTimeout发起的异步执行作为起始,
            周期内的其他异步执行操作均通过script请求实现。
            (若该方法将在短时间内被频繁调用,可以将周期频率再设高一些,如100毫秒)
    */
    if (t - arguments.callee._prev_timeout_called < 150) {
        var cancel = false;
        var script = document.createElement("script");
        script.type = "text/javascript";
        // 采用无效的data uri sheme马上触发readystate变化
        script.src  = "data:text/javascript,";
        script.onreadystatechange = function () {
            // 由于在一次请求过程中script的readystate会变化多次,因此通过cancel标识来保证仅调用一次call方法
            if (!cancel) {
                d.canceller();
                d.call();
            }
        };
        d.canceller = function () {
            if (!cancel) {
                cancel = true;
                script.onreadystatechange = null;
                document.body.removeChild(script);
            }
        };
        // 不同于img元素,script元素需要添加到dom树中才会发起请求
        document.body.appendChild(script);
    } else {
        arguments.callee._prev_timeout_called = t;
        var id = setTimeout(function () { d.call() }, 0);
        d.canceller = function () { clearTimeout(id) };
    }
    if (fun) d.callback.ok = fun;
    return d;
};

    2.5. Deferred.call函数实现

Deferred.call = function (fun) {
    var args = Array.prototype.slice.call(arguments, 1);
        // 核心在Deferred.next
    return Deferred.next(function () {
        return fun.apply(this, args);
    });
};

    2.6. Deferred.loop函数实现

Deferred.loop = function (n, fun) {
    // 入参n类似于Python中range的效果
    // 组装循环的配置信息
    var o = {
        begin : n.begin || 0,
        end   : (typeof n.end == "number") ? n.end : n - 1,
        step  : n.step  || 1,
        last  : false,
        prev  : null
    };
    var ret, step = o.step;
    return Deferred.next(function () {
        function _loop (i) {
            if (i <= o.end) {
                if ((i + step) > o.end) {
                    o.last = true;
                    o.step = o.end - i + 1;
                }
                o.prev = ret;
                ret = fun.call(this, i, o);
                if (Deferred.isDeferred(ret)) {
                    return ret.next(function (r) {
                        ret = r;
                        return Deferred.call(_loop, i + step);
                    });
                } else {
                    return Deferred.call(_loop, i + step);
                }
            } else {
                return ret;
            }
        }
        return (o.begin <= o.end) ? Deferred.call(_loop, o.begin) : null;
    });
};

    上述代码的理解难点在于Deferred实例A的事件处理函数若返回一个新的Deferred实例B,而实例A的Deferred链表中原本指向Deferred实例C,那么当调用实例A的call方法时是实例C的事件处理函数先被调用,还是实例B的事件处理函数先被调用呢?这时只需细读 Deferred.prototype.call方法 的实现就迎刃而解了,答案是先调用实例B的事件处理函数哦!

    2.7. Deferred.parallel函数实现

Deferred.parallel = function (dl) {
    // 对入参作处理
    var isArray = false;
    if (arguments.length > 1) {
        dl = Array.prototype.slice.call(arguments);
        isArray = true;
    } else if (Array.isArray && Array.isArray(dl) || typeof dl.length == "number") {
        isArray = true;
    }

    var ret = new Deferred(), values = {}, num = 0;
    for (var i in dl) if (dl.hasOwnProperty(i)) (function (d, i) {
        // 若d为函数类型,则封装为Deferred实例
        // 若d既不是函数类型,也不是Deferred实例则报错哦!
        if (typeof d == "function") 
            dl[i] = d = Deferred.next(d);
        d.next(function (v) {
            values[i] = v;
            if (--num <= 0) {
                // 凑够数就触发事件处理函数
                if (isArray) {
                    values.length = dl.length;
                    values = Array.prototype.slice.call(values, 0);
                }
                ret.call(values);
            }
        }).error(function (e) {
            ret.fail(e);
        });
        num++;
    })(dl[i], i);

    // 当dl为空时触发Deferred实例的成功事件
    if (!num) Deferred.next(function () { ret.call() });
    ret.canceller = function () {
        for (var i in dl) if (dl.hasOwnProperty(i)) {
            dl[i].cancel();
        }
    };
    return ret;
};

    通过源码我们可以知道parallel的入参必须为函数或Deferred实例,否则会报错哦!

    2.8. Deferred.earlier函数实现

Deferred.earlier = function (dl) {
    // 对入参作处理
    var isArray = false;
    if (arguments.length > 1) {
        dl = Array.prototype.slice.call(arguments);
        isArray = true;
    } else if (Array.isArray && Array.isArray(dl) || typeof dl.length == "number") {
        isArray = true;
    }
    var ret = new Deferred(), values = {}, num = 0;
    for (var i in dl) if (dl.hasOwnProperty(i)) (function (d, i) {
        // d只能是Deferred实例,否则抛异常
        d.next(function (v) {
            values[i] = v;
            // 一个Deferred实例触发成功事件则终止其他Deferred实例触发成功事件了
            if (isArray) {
                values.length = dl.length;
                values = Array.prototype.slice.call(values, 0);
            }
            ret.call(values);
            ret.canceller();
        }).error(function (e) {
            ret.fail(e);
        });
        num++;
    })(dl[i], i);

    // 当dl为空时触发Deferred实例的成功事件
    if (!num) Deferred.next(function () { ret.call() });
    ret.canceller = function () {
        for (var i in dl) if (dl.hasOwnProperty(i)) {
            dl[i].cancel();
        }
    };
    return ret;
};

    通过源码我们可以知道earlier的入参必须为Deferred实例,否则会报错哦!

    2.9. Deferred.chain函数实现

Deferred.chain = function () {
    var chain = Deferred.next();
    // 生成Deferred实例链表,链表长度等于arguemtns.length
    for (var i = 0, len = arguments.length; i < len; i++) (function (obj) {
        switch (typeof obj) {
            case "function":
                var name = null;
                // 通过函数名决定是订阅成功还是失败事件
                try {
                    name = obj.toString().match(/^\s*function\s+([^\s()]+)/)[1];
                } catch (e) { }
                if (name != "error") {
                    chain = chain.next(obj);
                } else {
                    chain = chain.error(obj);
                }
                break;
            case "object":
                // 这里的object包含形如{0:function(){}, 1: Deferred实例}、Deferred实例
                chain = chain.next(function() { return Deferred.parallel(obj) });
                break;
            default:
                throw "unknown type in process chains";
        }
    })(arguments[i]);
    return chain;
};

    2.10. Deferred.connect函数实现

Deferred.connect = function (funo, options) {
    var target, // 目标函数所属的对象
        func, // 目标函数
        obj; // 配置项
    if (typeof arguments[1] == "string") {
        target = arguments[0];
        func   = target[arguments[1]];
        obj    = arguments[2] || {};
    } else {
        func   = arguments[0];
        obj    = arguments[1] || {};
        target = obj.target;
    }

    // 预设定的入参
    var partialArgs       = obj.args ? Array.prototype.slice.call(obj.args, 0) : [];
    // 指出成功事件的回调处理函数位于原函数的入参索引
    var callbackArgIndex  = isFinite(obj.ok) ? obj.ok : obj.args ? obj.args.length : undefined;
    // 指出失败事件的回调处理函数位于原函数的入参索引
    var errorbackArgIndex = obj.ng;

    return function () {
        // 改造成功事件处理函数,将预设入参和实际入参作为成功事件处理函数的入参
        var d = new Deferred().next(function (args) {
            var next = this._next.callback.ok;
            this._next.callback.ok = function () {
                return next.apply(this, args.args);
            };
        });

        // 合并预设入参和实际入参
        var args = partialArgs.concat(Array.prototype.slice.call(arguments, 0));
        // 打造func的成功事件处理函数,内部将触发d的成功事件
        if (!(isFinite(callbackArgIndex) && callbackArgIndex !== null)) {
            callbackArgIndex = args.length;
        }
        var callback = function () { d.call(new Deferred.Arguments(arguments)) };
        args.splice(callbackArgIndex, 0, callback);

        // 打造func的失败事件处理函数,内部将触发d的失败事件
        if (isFinite(errorbackArgIndex) && errorbackArgIndex !== null) {
            var errorback = function () { d.fail(arguments) };
            args.splice(errorbackArgIndex, 0, errorback);
        }
        // 相当于setTimeout(function(){ func.apply(target, args) })
        Deferred.next(function () { func.apply(target, args) });
        return d;
    };
};

     如何简化将setTimeout、setInterval、XmlHttpRequest等异步API封装为Deferred对象(或Promise)对象的步骤是一件值思考的事情,而jsDeferred的connect类方法提供了一个很好的范本。

    2.11. Deferred.register函数实现

Deferred.register = function (name, fun) {
    this.prototype[name] = function () {
        var a = arguments;
        return this.next(function () {
            return fun.apply(this, a);
        });
    };
};

Deferred.register("loop", Deferred.loop);
Deferred.register("wait", Deferred.wait);

    2.12. Deferred.retry函数实现

Deferred.retry = function (retryCount, funcDeferred, options) {
    if (!options) options = {};

    var wait = options.wait || 0; // 尝试的间隔时间,存在最小时间精度所导致的延时问题
    var d = new Deferred();
    var retry = function () {
        // 有funcDeferred内部触发事件
        var m = funcDeferred(retryCount);
        m.next(function (mes) {
                d.call(mes);
            }).
            error(function (e) {
                if (--retryCount <= 0) {
                    d.fail(['retry failed', e]);
                } else {
                    setTimeout(retry, wait * 1000);
                }
            });
    };
    // 异步执行retry方法
    setTimeout(retry, 0);
    return d;
};

    2.13. Deferred.repeat函数实现

Deferred.repeat = function (n, fun) {
    var i = 0, end = {}, ret = null;
    return Deferred.next(function () {
        var t = (new Date()).getTime();
        // 当fun的执行耗时小于20毫秒,则马上继续执行下一次的fun;
        // 若fun的执行耗时大于20毫秒,则将UI线程控制权交出,并将异步执行下一次的fun。
        // 从而降低因循环执行耗时操作使页面卡住的风险。
        do {
            if (i >= n) return null;
            ret = fun(i++);
        } while ((new Date()).getTime() - t < 20);
        return Deferred.call(arguments.callee);
    });
};

五、总结                                

  通过剖析jsDeferred源码我们更深刻地理解Promises/A和Promises/A+规范,也了解到setTimeout的延时问题和通过img、script等事件缩短延时的解决办法(当然这里并没有详细记录解决办法的细节),最重要的是吸取大牛们的经验和了解API设计的艺术。但这里我提出一点对jsDeferred设计上的吐槽,就是Deferred实例的私有成员还是可以通过实例直接引用,而不像Promises/A官网实现示例那样通过闭包隐藏起来。

  尊重原创,转载请注明来自:www.cnblogs.com/fsjohnhuang…  ^_^肥子John

六、参考                                

《JavaScript框架设计》

jsDeferred官网


一、前言                              

  Promises/A是由CommonJS组织制定的异步模式编程规范,有不少库已根据该规范及后来经改进的Promises/A+规范提供了实现

  如QBluebirdwhenrsvp.js, mmDeferred, jQuery.Deffered()等。

  虽然上述实现库均以Promises/A+规范作为实现基准,但由于Promises/A+是对Promises/A规范的改进和增强,因此深入学习Promises/A规范也是不可缺少的。

  本文内容主要根据以下内容进行学习后整理而成,若有纰漏请各位指正,谢谢。

  www.promisejs.org/

  wiki.commonjs.org/wiki/Promis…

  由于篇幅较长特设目录一坨

  二、从痛点出发

  三、从感性领悟

  四、Promises/A的API规范

  五、通过示例看特性

  六、官方实例的源码剖析

     1. 基础功能部分1.1. 构造函数1.2. then函数的实现

     2. 辅助功能部分2.1. Promise.resolve实现2.2. Promise.reject实现2.3. Promise.all实现2.4. Promise.race实现

  七、总结

  八、参考

二、从痛点出发                           

  js中最常见的异步编程方式我想应该非回调函数不可了,优点是简单明了。但如果要实现下面的功能——非连续移动的动画效果,那是否还那么简单明了呢?

var left = function(cb){
  el.style.left = (el.offsetLeft + 200) + 'px';
  cb && cb();
};

var el = document.getElementById('test');
// 臭名远播的“回调地狱(callback hell)”
setTimeout(function(){
  left(function(){
    setTimeout(function(){
       left(function(){
         setTimeout(function(){
           left();
         },2000);
       });
    }, 2000);
  });
}, 2000);

  傻眼了吧!下面我们看一下使用Promises/A规范异步模式的编码方式吧!

var el = document.getElementById('test');
// 定义thenable对象
var thenable = {then: function(resolve, reject){
  setTimeout(function(){
    el.style.left = (el.offsetLeft + 200) + 'px';
    // 触发promise对象链中的下一个状态为pending的promise对象的状态变为fulfilled
    resolve && resolve(); 
   }, 2000);
}};
// 将thenable对象封装为promise对象
var promise = Promise.resolve(thenable);
// 订阅promise对象状态变化时所触发的事件
promise
      .then(function(){
         return thenable;
      })
      .then(function(){
         return thenable;
      });

  也许你有着“我不入地狱谁入地狱”的豪情,但如果现在需求改为循环10次执行非连续移动呢?20次呢?这时我想连地狱的门在哪也难以找到了,但Promises/A的方式让我们轻松应对。

var el = document.getElementById('test');
var thenable = {then: function(resolve, reject){
  setTimeout(function(){
    el.style.left = (el.offsetLeft + 200) + 'px';
    resolve && resolve();
   }, 2000);
}};
var promise = Promise.resolve(thenable);
var i = 0, count = 10;
while (++i < 10){
  promise = promise.then(function(){
     return thenable;
  });
}

三、 从感性领悟                            

  也许通过上述代码我们已经了解到Promise可以让我们以不同的方式编写异步代码,但也仅仅停留在按形式套代码的层面上而已,只有从感性出发理解Promise的设计思想我想才能更好地掌握。以下内容是读张鑫旭的《es6-javascript-promise-感性认知》和《js算法与思维方式》后的所思所想。

  首先举一个生活的例子,看看实际生活的处理逻辑和通过代的处理逻辑有什么出入吧!

  例子:下班搭车去接小孩回家。

  直觉思维分解上述句子会得出以下任务及顺序:下班->搭车->到幼儿园(小学等)接小孩->(走路)回家。可以看到这种思维方式是任务+执行顺序的,丝毫没有带任务间的时间距离。于是同步代码可以写成:

var 下班 = function(){};
var 搭车 = function(){};
var 接小孩 = function(){};
var 回家 = function(){};

下班();
搭车();
接小孩();
回家();

  但实际执行时各任务均有可能因某些原因出现不同程度的延时,如下班时老板突然安排一项新任务(推迟10分钟下班),错过班车(下一班要等10分钟),小孩正在搞卫生(20分钟后搞完),回家。真实生活中我们能做的就是干等或做点其他事等到点后再继续之前的流程!但程序中尤其是像JS这样单线程程序是“等”不起的,于是出现异步模式:

var 下班 = function(nextTask){};
var 搭车 = function(nextTask){};
var 接小孩 = function(nextTask){};
var 回家 = function(nextTask){};

下班(function(){
  setTimeout(function(){
    搭车(function(){
       setTimeout(function(){
         接小孩(function(){
           setTimeout(function(){
             回家(); 
           },  20*60*1000);
         });
       },  10*60*1000);
    });
  }, 10*60*1000);
});

  当回头再看上面这段代码时,会发现整个流程被任务间的时间距离拉得很远“下班------------搭车------------接小孩-------------------回家”。回想一下真实生活中我们即使执行每个任务时均需要等待,但整个流程的抽象也只是“下班,等,搭车,等,接小孩,等,回家”。因此回调函数的异步模式与我们的思维模式相距甚远,那么如何做到即告诉程序任务间的时间距离,又从代码结构上淡化这种时间距离感呢?而Promise就是其中一种方式了!

  从开发者角度(第三人称)来看Promise作为任务间的纽带存在,流程被抽象为“下班,promise,搭车,promise,接小孩,promise,回家”,而任务间的时间距离则归并到任务本身而已。从程序执行角度(第一人称)来看Promise为一个待定变量,但结果仅有两种——成功和失败,于是仅对待定变量设定两种结果的处理方式即可。

// 任务定义部分
var 下班 = function(){};下班.then = function(resovle){ 
  setTimeout(function(){
    resovle();
  }, 10*60*1000);
};
var 搭车 = function(){};
搭车.then = function(resovle){ 
  setTimeout(function(){
    resovle();
  }, 10*60*1000);
};
var 接小孩 = function(){};
接小孩.then = function(resovle){ 
  setTimeout(function(){
    resovle();
  }, 20*60*1000);
};
var 回家 = function(){};

// 流程部分
var p = new Promise(function(resolve, reject){
  resolve();
});
p.then(function(){
   下班();
   return 下班;
 })
 .then(function(){
   搭车();
   return 搭车;
 })
 .then(function(){
   接小孩();
   return 接小孩;
 })
 .then(function(){
   回家();
 });

 看代码结构被拉平了,但代码结构的变化是表象,最根本的是任务间的时间距离被淡化了,当我们想了解工作流程时不会被时间距离分散注意力,当我们想知道各个任务的延时时只需查看任务定义本身即可,这就是关注点分离的一种表现哦!

四、Promises/A的API规范                      

  经过上述示例我想大家已经尝到了甜头,并希望掌握这一武器从而逃离回调地狱的折磨了。下面就一起了解Promise及其API规范吧!

  1. 有限状态机

    Promise(中文:承诺)其实为一个有限状态机,共有三种状态:pending(执行中)、fulfilled(执行成功)和rejected(执行失败)。

    其中pending为初始状态,fulfilled和rejected为结束状态(结束状态表示promise的生命周期已结束)。

    状态转换关系为:pending->fulfilled,pending->rejected。

    随着状态的转换将触发各种事件(如执行成功事件、执行失败事件等)。  

  2. 构造函数

    Promise({Function} factory/*({Function} resolve, {Function} reject)*/) ,构造函数存在一个Function类型的入参factory,作为唯一一个修改promise对象状态的地方,其中factory函数的入参resolve的作用是将promise对象的状态从pending转换为fulfilled,而reject的作用是将promise对象的状态从pending转换为rejected。

    入参 void resolve({Any} val)  ,当val为非thenable对象和promise对象时则会将val作为执行成功事件处理函数的入参,若val为thenable对象时则会执行thenable.then方法,若val为Promise对象时则会将该Promise对象添加到Promise对象单向链表中。

    入参 void reject({Any} reason) ,reason不管是哪种内容均直接作为执行失败事件处理函数的入参。

    注意:关于抛异常的做法,同步模式为 throw new Error("I'm synchronous way!") ,而Promise规范的做法是 reject(new Error("I'm asynchronous way!")); 

  3. 实例方法

     Promise then([{Function} onFulfilled[, {Function} onRejected]]) ,用于订阅Promise对象状态转换事件,入参onFulfilled为执行成功的事件处理函数,入参onRejected为执行失败的事件处理函数。两者的返回值均作为Promise对象单向链表中下一个Promise对象的状态转换事件处理函数的入参。而then方法的返回值是一个新的Promise对象并且已添加到Promise对象单向链表的末尾。

     Promise catch({Function} onRejected) ,相当于 then(null, onRejected) 。

  4. 类方法

     Promise Promise.resolve({Any} obj) ,用于将非Promise类型的入参封装为Promise对象,若obj为非thenable对象则返回状态为fulfilled的Promise对象,对于非若入参为Promise对象则直接返回。

     Promise Promise.reject({Any} obj) ,用于将非Promise类型的入参封装为状态为rejected的Promise对象。

     Promise Promise.all({Array} array) ,当array中所有Promise实例的状态均为fulfilled时,该方法返回的Promise对象的状态也转为fulfilled(执行成功事件处理函数的入参为array数组中所有Promise实例执行成功事件处理函数的返回值),否则转换为rejected。

     Promise Promise.race({Array} array) ,当array中有一个Promise实例的状态出现fulfilled或rejected时,该方法返回的Promise对象的状态也转为fulfilled或rejected。

  5. thenable对象

    拥有 then方法 的对象均称为thenable对象,并且thenable对象将作为Promise对象被处理。

五、通过示例看特性                          

  单看接口API是无法掌握Promise/A的特性的,下面通过示例说明:

示例1——链式操作+执行最近的事件处理函数

// 创建Promise实例p1
var p1 = new Promise(function(resovle, reject){
  setTimout(function(){
    console.log('hello1');
    // 1秒后修改promise实例的状态为fulfilled
    resolve('hello1');
  },1000);
});
// 订阅p1的执行成功事件处理函数,并创建Promise实例p2
// 该处理函数将立即返回结果
var p2 = p1.then(function(val){
  var newVal = 'hello2';
  console.log(val);
  console.log(newVal);
  return newVal;
})
// 订阅p2的执行成功事件处理函数,并创建Promise实例p3
// 该处理函数返回一个Promise实例,并1秒后该Promise实例的状态转换为rejected
var p3 = p2.then(function(val){
  console.log(val);
  var tmp = new Promise(function(resolve, reject){
     setTimout(function(){
       reject(new Error('my error!'));
     }, 1000);
  });
  return tmp;
});
// 订阅p3的执行成功事件处理函数,并创建Promise实例p4
// 由于p2的处理函数所返回的Promise实例状态为rejected,因此p3的执行成功事件处理函数将不被执行,并且p3没有执行失败事件处理函数,因此会将控制权往下传递给p4的执行失败事件处理函数。
var p4 = p3.then(function(val){
  console.log('skip');
})
//  订阅p4的执行成功事件处理函数,并创建Promise实例p5
var p5 = p4.catch(function(reason){
  console.log(reason);
});、

该示例的结果为:hello1     hello1    hello2    hello2    error:my error!。

示例2——事件处理函数晚绑定,同样可被触发

var p1 = new Promise(function(resolve, reject){
  resolve('hello');
});
// Promise实例p1状态转换为fulfilled一秒后才绑定事件处理函数
setTimeout(function(){
  p1.then(function(val){
    console.log(val);
  });
}, 1000);

该示例的结果为: hello

六、官方实现的源码剖析                      

由于Promises/A规范实际仅提供接口定义,并没有规定具体实现细节,因此我们可以先自行作实现方式的猜想。

上述的示例1表明Promise是具有链式操作,因此Promise的内部结构应该是一个单向链表结构,每个节点除了自身数据外,还有一个字段用于指向下一个Promise实例。

构造函数的具体实现可以是这样的

var Promise = exports.Promise = function(fn){
  if (!(this instanceof iPromise))
    return new iPromise(fn);
  var _ = this._ = {};
  _.state = 0; // 0:pending, 1:fulfilled, 2:rejected
  _.thenables = []; // 单向链表
  fn && fn(this.resolve.bind(this), this.reject.bind(this));
};

而then函数的具体实现为

Promise.prototype.then = function(fulfilledHandler, rejectedHandler){
  var _ = this._;
  var promise = new Promise();
   // 单向链表的逻辑结构
  var thenable = {
    onFulfilled: fulfilledHandler, // 执行成功的事件处理函数
    onRejected: rejectedHandler, // 执行失败的事件处理函数
    promise: promise // 下一个Promise实例
  };
  _.thenables.push(thenable);
  if (_.state !== 0){
    window[window.setImmediate ? 'setImmediate' : 'setTimeout'].call(window, function(){
      handle(_);
    }, 0);
  }
  return promise;
};

 但官方提供的实现方式却比上述思路晦涩得多(源码含适配nodejs和浏览器端的额外代码干扰视线,因此我提取可直接在浏览器上使用的主逻辑部分出来,具体代码请浏览:github.com/fsjohnhuang…)

 1. 基础功能部分

    基础功能部分主要分为 构造函数then函数的实现 两部分,而 then函数的实现是理解的难点

    1.1. 构造函数

var Promise = exports.Promise = function (fn) {
      if (typeof this !== "object") throw new TypeError("Promises must be constructed via new");
      if (typeof fn !== "function") throw new TypeError("not a function");
      var state = null; // 状态,null:pending,true:fulfilled,false:rejected
      var value = null; // 当前promise的状态事件处理函数(onFulfilled或onRejected)的入参
      var deferreds = []; // 当前promise的状态事件处理函数和promise链表中下一个promise的状态转换发起函数
      var self = this;
      // 唯一的公开方法
      this.then = function(onFulfilled, onRejected) {
        return new self.constructor(function(resolve, reject) {
          handle(new Handler(onFulfilled, onRejected, resolve, reject));
        });
      };
      // 保存和执行deferreds数组中的元素
      function handle(deferred) {
        if (state === null) {
          deferreds.push(deferred);
          return;
        }
        // asap的作用为将入参的操作压入event loop队列中
        asap(function() {
          var cb = state ? deferred.onFulfilled : deferred.onRejected;
          if (cb === null) {
            (state ? deferred.resolve : deferred.reject)(value);
            return;
          }
          var ret;
          try {
            // 执行当前promise的状态转换事件处理函数
            ret = cb(value);
          } catch (e) {
            // 修改promise链表中下一个promise对象的状态为rejected
            deferred.reject(e);
            return;
          }
          // 修改promise链表中下一个promise对象的状态为fulfilled
          deferred.resolve(ret);
        });
      }
      // promise的状态转换发起函数,触发promise的状态从pending->fulfilled
      function resolve(newValue) {
        try {
          if (newValue === self) throw new TypeError("A promise cannot be resolved with itself.");
          if (newValue && (typeof newValue === "object" || typeof newValue === "function")) {
            var then = newValue.then;
            if (typeof then === "function") {
              // 将控制权移交thenable和promise对象,由它们来设置当前pormise的状态和状态转换事件处理函数的实参
              doResolve(then.bind(newValue), resolve, reject);
              return;
            }
          }
          state = true;
          value = newValue;
          finale();
        } catch (e) {
          reject(e);
        }
      }
      // promise的状态转换发起函数,触发promise的状态从pending->rejected
      function reject(newValue) {
        state = false;
        value = newValue;
        finale();
      }
      // 向链表的下一个promise移动
      function finale() {
        for (var i = 0, len = deferreds.length; i < len; i++) handle(deferreds[i]);
        deferreds = null;
      }
      // 执行构造函数的工厂方法,由工厂方法触发promise的状态转换
      doResolve(fn, resolve, reject);
    }

     我们可以通过 new Promise(function(resolve, reject){ resolve('hello'); }); 来跟踪一下执行过程,发现重点在 doResolve(fn, resolve, reject) 方法调用中,该方法定义如下:

// 对状态转换事件处理函数进行封装后,再传给执行函数
    function doResolve(fn, onFulfilled, onRejected) {
      // done作为开关以防止fn内同时调用resolve和reject方法
      var done = false;
      try {
        fn(function(value) {
          if (done) return;
          done = true;
          onFulfilled(value);
        }, function(reason) {
          if (done) return;
          done = true;
          onRejected(reason);
        });
      } catch (ex) {
        if (done) return;
        done = true;
        onRejected(ex);
      }
    }

     doResovle仅仅是对resolve和reject方法进行封装以防止同时被调用的情况而已,这时控制权到达 resolve方法 。由于resovle的入参为字符串类型,因此直接修改当前promise的状态和保存状态转换事件处理函数的实参即可(若resolve的入参为thenable对象或Promise对象,则将控制权交给该对象,由该对象来设置当前promise的状态和状态转换事件处理函数的实参),然后将控制权移交 finale方法 。finale方法内部会遍历deffereds数组并根据状态调用对应的处理函数和修改promise链表中下一个promise对象的状态。

     那么deffereds数组具体是什么呢?其实它就跟我之前猜想的thenables数组功能一致,用于保存状态转换事件处理函数和维护promise单向链表(不直接存放下一个promise对象的指针,而是存放下一个promise的resovle和reject方法)的。具体数据结构如下:

// 构造promise的链表逻辑结构
function Handler(onFulfilled, onRejected, resolve, reject) {
  this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; // 当前promise的状态转换事件处理函数
  this.onRejected = typeof onRejected === "function" ? onRejected : null; // 当前promise的状态转换事件处理函数
  this.resolve = resolve; // 设置链表中下一个promise的状态为fulfilled
  this.reject = reject; // 设置链表中下一个promise的状态为rejected
}

    若当前promise有deffered实例,那么则会执行handle函数中asap函数的函数入参

function() {
  var cb = state ? deferred.onFulfilled : deferred.onRejected;
  if (cb === null) {
    (state ? deferred.resolve : deferred.reject)(value);
      return;
  }
  var ret;
  try {
    // 执行当前promise的状态转换事件处理函数
     ret = cb(value);
  } catch (e) {
    // 修改promise链表中下一个promise对象的状态为rejected
     deferred.reject(e);
     return;
  }
  // 修改promise链表中下一个promise对象的状态为fulfilled
  deferred.resolve(ret);}

     我觉得原实现方式不够直白,于是改成这样:

function(){
 var cb = deferred[state ? 'onFulfilled' : 'onRejected'];
 var deferredAction = 'resolve', ret;
 try{
   ret = cb ? cb(value) : value;
 }
 catch (e){
   ret = e;
    deferredAction = 'reject';
 }
 deferred[deferredAction].call(deferred, ret);
}

   文字太多了,还是看图更清楚哦!

 

   接下来的问题就是deffereds数组的元素是从何而来呢?那就要看看then函数了。

    1.2. then函数的实现

      then函数代码结构上很简单,但设计上却很精妙。

this.then = function(onFulfilled, onRejected){
  return new self.constructor(function(resolve, reject) {
     handle(new Handler(onFulfilled, onRejected, resolve, reject));
  });
}
原格式

      为了好看些,我修改了一下格式:

this.then = function(onFulfilled, onRejected) {
  // 构造新的promise实例并返回,从而形成链式操作
   return new Promise(function(resolve, reject) {
     var handler = new Handler(onFulfilled, onRejected, resolve, reject);
    /* 
          注意:这里利用了闭包特性,此处的handle并不是新Promise的handle函数,而是this.then所属promise的handle函数。
              因此handler将被添加到this.then所属promise的deffereds数组中。
              而onFulfilled和onRejected自然成为了this.then所属promise的状态转换事件处理函数,
              而resolve和reject依旧是新promise实例的状态转换触发函数。
          */
      handle(handler);
   });
};

    源码读后感:

      通过闭包特性来让链表后一个对象调用前一个对象的方法和变量,从而实现私有成员方法和属性实在是过瘾。比起我猜想的实现方式通过下划线(_)提示API调用者该属性下的均为私有成员的做法封装性更完整。

 2. 辅助功能部分

    辅助功能部分主要就是Promise.resolvePromise.rejectPromise.allPromsie.race的实现,它们均由基础功能扩展而来。

    2.1. Promise.resolve实现

      作用:将非Promise对象转换为Promise对象,而非Promise对象则被细分为两种:thenable对象和非thenable对象。

         thenable对象的then将作为Promise构造函数的工厂方法被调用

         非thenable对象(Number、DOMString、Boolean、null、undefined等)将作为pending->fulfilled的事件处理函数的入参。

     由于源码中加入性能优化的代码,因此我提出核心逻辑以便分析:

// 将非thenable对象构造为thenable对象
// 其then方法则返回一个真正的Promise对象
function ValuePromise(value) {
  this.then = function(onFulfilled) {
     if (typeof onFulfilled !== "function") return this;
      return new Promise(function(resolve, reject) {
        asap(function() {
          try {
             resolve(onFulfilled(value));
           } catch (ex) {
             reject(ex);
           }
        });
     });
   };
}/*
 也可以将非thenable对象构造为Promise对象
 function ValuePromise(value){
   return new Promise(function(resolve){
      resolve(value);
    });
 }*/

 Promise.resolve = function(value) {
      if (value instanceof Promise) return value;
      if (typeof value === "object" || typeof value === "function") {
        try {
          var then = value.then;
          if (typeof then === "function") {
            return new Promise(then.bind(value));
          }
        } catch (ex) {
          return new Promise(function(resolve, reject) {
            reject(ex);
          });
        }
      }
      return new ValuePromise(value);
    };

    2.2. Promise.reject实现

         作用:创建一个状态为rejected的promise对象,且入参将作为onRejected函数的入参。

 Promise.reject = function(value) {
      return new Promise(function(resolve, reject) {
        reject(value);
      });
    };

    2.3. Promise.all实现

         作用:返回的一个promise实例,且该实例当且仅当Promise.all入参数组中所有Promise元素状态均为fulfilled时该返回的promise实例的状态转换为fulfilled(onFulfilled事件处理函数的入参为处理结果数组),否则转换为rejected。

Promise.all = function(arr) {
      var args = Array.prototype.slice.call(arr);
      return new Promise(function(resolve, reject) {
        if (args.length === 0) return resolve([]);
        var remaining = args.length;
        function res(i, val) {
          try {
            if (val && (typeof val === "object" || typeof val === "function")) {
              var then = val.then;
              if (typeof then === "function") {
                then.call(val, function(val) {
                  // 对于thenable和promise对象则订阅onFulfilled事件获取处理结果值
                  res(i, val);
                }, reject);
                return;
              }
            }
            args[i] = val;
            // 检测是否所有入参都已返回值
            if (--remaining === 0) {
              resolve(args);
            }
          } catch (ex) {
            reject(ex);
          }
        }
        for (var i = 0; i < args.length; i++) {
          res(i, args[i]);
        }
      });
    };

    2.4. Promise.race实现

       作用:返回一个promise对象,且入参数组中一旦某个promise对象状态转换为fulfilled,则该promise对象的状态也转换为fulfilled。

Promise.race = function(values) {
  return new Promise(function(resolve, reject) {
     values.forEach(function(value) {
        // 将数组元素转换为promise对象
        Promise.resolve(value).then(resolve, reject);
     });
   });
};

    源码实现的方式是即使第一个数组元素的状态已经为fulfilled,但仍然会订阅其他元素的onFulfilled和onRejected事件,依赖resolve函数中的标识位done来保证返回的promise对象的onFulfilled函数仅执行一次。我修改为如下形式:

 Promise.race = function(values){
      return new Promise(function(resolve, reject){
        var over = 0;
        for (var i = 0, len = values.length; i < len && !over; ++i){
          var val = values[i];
          if (val && typeof val.then === 'function'){
            val.then(function(res){
              !over++ && resolve(res);
            }, reject);
          }
          else{
            !over++ && resolve(val);
          }
        }
      });
    };

七、总结                               

  虽然通过Promises/A规范进行异步编程已经舒坦不少,但该规范仍然不够给力,于是出现了Promises/A+规范。后面我们继续探讨Promises/A+规范吧!

  尊重原创,转载请注明来自:www.cnblogs.com/fsjohnhuang… ^_^肥仔John

八、参考                                    

http://javascript.ruanyifeng.com/advanced/asynchronous.htm

http://www.zhangxinxu.com/wordpress/2014/02/es6-javascript-promise-%E6%84%9F%E6%80%A7%E8%AE%A4%E7%9F%A5/comment-page-1/