JavaScript 异步编程之 jsdeferred 原理解析

秋涛小哥_1424

秋涛小哥_1424

2016-11-07

1315人浏览

原创

1. 前言

最近在看司徒正美的《javascript框架设计》,看到异步编程的那一章介绍了jsdeferred这个库,觉得很有意思,花了几天的时间研究了一下代码,在此做一下分享。

异步编程是编写js的一个很重要的理念,特别是在处理复杂应用的时候,异步编程的技巧就至关重要。那么下面就来看看这个被称为里程碑式的异步编程库吧。

2. API源码解析

2.1 构造函数

这里使用了安全的构造函数,避免了在没有使用new调用构造函数时出错的问题,提供了两个形式俩获取Deferred对象实例。

function Deferred() {
    return (this instanceof Deferred) ? this.init() : new Deferred();
}

// 方式1 
var o1 = new Deferred();
// 方式2
var o2 = Deferred();

2.2 Deferred.define()

这个方法可以包装一个对象,指定对象的方法,或者将Deferred对象的方法直接暴露在全局作用域下,这样就可以直接使用。

Deferred.methods = ["parallel", "wait", "next", "call", "loop", "repeat", "chain"];
/*
    @Param obj 赋予该对象Deferred的属性方法
    @Param list 指定属性方法
*/
Deferred.define = function(obj, list){
    if(!list)list = Deferred.methods;
    // 获取全局作用域的技巧,利用立即执行函数的作用域为全局作用域的技巧
    if(!obj) obj = (function getGlobal(){return this})();
    // 将属性都挂载到obj上
    for(var i = 0; i <p style="box-sizing: border-box; font-family: " helvetica neue arial sc sans gb micro hei yahei sans-serif font-weight: line-height: color: rgb margin: font-size: white-space: normal background-color:>2.3 异步的操作实现</p><p>在JSDeferred中有许多异步操作的实现方式,也是作为这个框架最为出彩的地方,方法依次是:</p><p>script.onreadystatechange(针对IE5.5~8)</p><p>img.onerror/img.onload(针对现代浏览器的异步操作方法)</p><p>针对node环境的,使用process.nextTick来实现异步调用(已经过时)</p><p>setTimeout(default)</p><p>它会视浏览器选择最快的API。</p><p>使用script的onreadystatechange事件来进行,需要注意的是由于浏览器对并发请求数有限制,(IE5.5~8为2~3,IE9+和现代浏览器为6),当并发请求数大于上限时,会让请求的发起操作排队执行,导致延时更严重。代码的思路是以150ms为一个周期,每个周期以通过setTimeout发起的异步执行为起始,周期内的其他异步执行操作通过script请求实现,如果此方法被频繁调用的话,说明达到并发请求数上限的可能性越高,因此可以下调一下周期时间,例如设为100ms,避免因排队导致的高延时。</p><pre class="brush:js;toolbar:false">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();
if(t - arguments.callee._prev_timeout_called <p>使用img的方式,利用src属性报错和绑定事件回调的方式来进行异步操作</p><pre class="brush:js;toolbar:false">Deferred.next_faster_way_Image = ((typeof window === "object") &&
(typeof Image != "undefined") && 
!window.opera && document.addEventListener) && 
function (fun){
var d = new Deffered();
var img = new Image();
var hander = 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);
}
// 赋值一个错误的URL
img.src = "data:imag/png," + Math.random();
if(fun) d.callback.ok = fun;
return d;
}

针对Node环境的,使用process.nextTick来实现异步调用

Deferred.next_tick = (typeof process === 'object' &&
typeof process.nextTick === 'function') && 
function (fun) {
var d = new Deferred();
process.nextTick(function() { d.call() });
if (fun) d.callback.ok = fun;
return d;
};

setTimeout的方式,这种方式有一个触发最小的时间间隔,在旧的IE浏览器中,时间间隔可能会稍微长一点(15ms)。

Deferred.next_default = function (fun) {
var d = new Deferred();
var id = setTimeout(function(){
clearTimeout(id);
d.call(); // 唤起Deferred调用链
}, 0)
d.canceller = function () {
try{
    clearTimeout(id);
}catch(e){}
};
if(fun){
d.callback.ok = fun;
}
return d;
}

默认的顺序为

Deferred.next = 
    Deferred.next_faster_way_readystatechange || // 处理IE
    Deferred.next_faster_way_Image || // 现代浏览器
    Deferred.next_tick || // node环境
    Deferred.next_default; // 默认行为

根据JSDeferred官方的数据,使用next_faster_way_readystatechange和next_faster_way_Image这两个比原有的setTimeout异步的方式快上700%以上。

看了一下数据,其实对比的浏览器版本都相对比较旧,在现代的浏览器中性能提升应该就没有那么明显了。

2.4 原型方法

Deferred的原型方法中实现了

_id 用来判断是否是Deferred的实例,原因好像是Mozilla有个插件也叫Deferred,因此不能通过instanceof来检测。cho45于是自定义标志位来作检测,并在github上提交fxxking Mozilla。

init 初始化,给每个实例附加一个_next和callback属性

next 用于注册调用函数,内部以链表的方式实现,节点为Deferred实例,调用的内部方法_post

error 用于注册函数调用失败时的错误信息,与next的内部实现一致。

call 唤起next调用链

fail 唤起error调用链

cancel 执行cancel回调,只有在唤起调用链之前调用才有效。(调用链是单向的,执行之后就不可返回)

Deferred.prototype = {
    _id : 0xe38286e381ae, // 用于判断是否是实例的标识位
    init : function () {
        this._next = null; // 一种链表的实现思路
        this.callback = {
            ok : Deferred.ok, // 默认的ok回调
            ng : Deferred.ng  // 出错时的回调
        };
        return this;
    },
    next : function (fun) {
        return this._post("ok", fun); // 调用_post建立链表
    },
    error : function (fun) {
        return this._post("ng", fun); // 调用_post建立链表
    },
    call : function(val) {
        return this._fire("ok", val); // 唤起next调用链
    },
    fail : function (err) {
        return this._fire("ng", err); // 唤起error调用链
    },
    cancel : function () {
        (this.canceller || function () {}).apply(this);
        return this.init(); // 进行重置
    },
    _post : function (okng, fun){ // 建立链表
        this._next = new Deferred();
        this._next.callback[okng] = fun;
        return this._next;
    },
    _fire : function (okng, fun){
        var next = "ok";
        try{
            // 注册的回调函数中,可能会抛出异常,用try-catch进行捕捉
            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.wait方法使用的,
            value._next = this._next;
        } else { // 如果不是,则继续执行
            if (this._next) this._next._fire(next, value);
        }
        return this;
    }
}

2.5 辅助静态方法

上面的代码中,可以看到一些Deferred对象的方法(静态方法),下面简单介绍一下:

// 默认的成功回调
Deferred.ok = function (x) {return x};

// 默认的失败回调
Deferred.ng = function (x) {throw x};

// 根据_id判断实例的实现
Deferred.isDeferred = function (obj) {
    return !!(obj && obj._id === Deferred.prototype._id);
}

2.6 简单小结

看到这里,我们需要停下来,看看一个简单的例子,来理解整个流程。

Defferred对象自身有next属性方法,在原型上也定义了next方法,需要注意这一点,例如以下代码:

JavaScript 测试模式
JavaScript 测试模式

使用 Jest、Vitest 和 Testing Library 实施全面的测试策略,包括单元测试、集成测试和端到端测试,涵盖模拟、测试夹具和测试驱动开发。在编

下载
var o = {};
Deferred.define(o);
o.next(function fn1(){
    console.log(1);
}).next(function fn2(){
    console.log(2);
});

o.next()是Deffered对象的属性方法,这个方法会返回一个Defferred对象的实例,因此下一个next()则是原型上的next方法。

第一个next()方法将后续的代码变成异步操作,后面的next()方法实际上是注册调用函数。

在第一个next()的异步操作里面唤起后面next()的调用链(d.call()),开始顺序的调用,换句话说就是,fn1和fn2是同步执行的。

那么,如果我们希望fn1和fn2也是异步执行,而不是同步执行的,这就得借助Deferred.wait方法了。

2.7 wait & register

我们可以使用wait来让fn1和fn2变成异步执行,代码如下:

Deferred.next(function fn1() {
    console.log(1)
}).wait(0).next(function fn2() {
    console.log(2)
});

wait方法很有意思,在Deferred的原型上并没有wait方法,而是在静态方法上找到了。

Deferred.wait = function (n) {
    var d = new Deferred(),
        t = new Date();
    // 使用定时器来变成异步操作
    var id = setTimeout(function () {
        d.call((new Date()).getTime() - t.getTime());
    }, n * 1000);

    d.canceller = function () {
        clearTimeout(id);
    }
    return d;
}

那么这个方法是怎么放到原型上的?原来是通过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("wait", Deferred.wait);

我们需要思考为什么要用这种方式将wait方法register到Deferred的原型对象上去?,因为明显这种方式有点难以理解。

结合例子,我们进行讨论,便能够彻底地理解上述的问题。

Deferred.next(function fn1(){ // d1
    console.log(1);
})
.wait(1) // d2
.next(function fn2(){ // d3
    console.log(2);
});

这段代码首先会建立一个调用链

4153318316-581dfe86654b6_articlex.jpg

之后,执行的过程为(如图所示)

4153318316-581dfe86654b6_articlex.jpg

我们来看看执行过程的几个关键点

图中的d1、d2、d3、d_wait表示在调用链上生成的Deferred对象的实例

在调用了d2的callback.ok即包装了wait()方法的匿名函数之后,返回了在wait()方法中生成的Deferred对象的实例d_wait,保存在变量value中,在_fire()方法中有一个if判断

if(Deferred.isDeferred(value)){
    value._next = this._next;
}
在这里并没有继续往下执行调用链的函数,而是重新建立了一个调用链,此时链头为d_wait,在wait()方法中使用setTimeout,使其异步执行,使用d.call()重新唤起调用链。

理解了整个过程,就比较好回到上面的问题了。之所以使用register的方式是因为原型上的wait方法并非直接使用Deferred.wait,而是把Deferred.wait方法作为参数,对原型上的next()方法进行curry化,然后返回一个柯里化之后的next()方法。而Deferred.wait()其实和Deferred.next()的作用很类似,都是异步执行接下来的操作。

2.8 并归结果 parallel

设想一个场景,我们需要多个异步网络查询任务,这些任务没有依赖关系,不需要区分前后,但是需要等待所有查询结果回来之后才能进一步处理,那么你会怎么做?在比较复杂的应用中,这个场景经常会出现,如果我们采用以下的方式(见伪代码)

var result = [];
$.ajax("task1", function(ret1){
    result.push(ret1);
    $.ajax("task2", function(ret2){
        result.push(ret2);
        // 进行操作
    });
});

这种方式可以,但是却无法同时发送task1和task2(从代码上看还以为之间有依赖关系,实际上没有)。那怎么解决?这就是Deferred.parallel()所要解决的问题。

我们先来个简单的例子感受一下这种并归结果的方式。

Deferred.parallel(function () {
    return 1;
}, function () {
    return 2;
}, function () {
    return 3;
}).next(function (a) {
    console.log(a); // [1,2,3]
});

在parallel()方法执行之后,会将结果合并为一个数组,然后传递给next()中的callback.ok中。可以看到parallel里面都是同步的方法,先来看看parallel的源码是如何实现,再来看看能不能结合所学来改造实现我们所需要的ajax的效果。

Deferred.parallel = function (dl) {
    /* 
        前面都是对参数的处理,可以接收三种形式的参数 
        1. parallel(fn1, fn2, fn3).next()
        2. parallel({
                foo : $.get("foo.html"),
                bar : $.get("bar.html")
            }).next(function (v){
                v.foo // => foo.html data
                v.bar // => bar.html data
            });
        3. parallel([fn1, fn2, fn3]).next(function (v) {
                v[0] // fn1执行的结果
                v[1] // fn2执行的结果
                v[3] // fn3执行返回的结果
            });
    */
    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(), // 用于归并结果的Deferred对象的实例
        value = {}, // 收集函数执行的结果
        num = 0 ; // 计数器,当为0时说明所有任务都执行完毕
    
    // 开始遍历,这里使用for-in其实效率不高
    for (var i in dl) {
        // 预防遍历了所有属性,例如toString之类的
        if (dl.hasOwnProperty(i)) {
            // 利用闭包保存变量状态
            (function (d, i){
                // 使用Deferred.next()开始一个异步任务,并且执行完成之后,收集结果
                if (typeof d == "function") dl[i] = d = Deferred.next(d);
                d.next(function (v) {
                    values[i] = v;
                    if( --num <p>结合上述知识,我们可以在parallel中使用异步方法,代码如下</p><pre class="brush:js;toolbar:false">Deferred.parallel(function fn1(){
    var d = new Deferred();
    $.ajax("task1", function(ret1){
        d.call(ret1);
    });
    return d;
}, function () {
    var d = new Deferred();
    $.ajax("task2", function fn2(ret2) {
        d.call(ret2)
    });
    return d;
}).next(function fn3(ret) {
    ret[0]; // => task1返回的结果
    ret[1]; // => task2返回的结果
});

为什么可以这样?我们来图解一下,加深一下理解。

892670031-581ee02140c15_articlex.png

我们使用了_fire中的if判断,建立了新的调用链,获得去统计计数函数(即parallel中--num)的控制权,从而使得在parallel执行异步的方法。

Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

javascript

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
Go-Zero API开发使用教程
Go-Zero API开发使用教程

本专题聚焦Go-Zero的API开发,拆解.api描述文件的语法结构、路由与分组定义、参数绑定与校验规则,说明统一响应体、JWT鉴权、跨域与接口文档生成的落地方式,并给出网关聚合与接口测试的实践建议。

2026.11.12

80

15

Go-Zero安装与goctl环境配置教程
Go-Zero安装与goctl环境配置教程

本专题围绕Go-Zero的安装与工具链配置展开,讲清Go版本要求、goctl安装方式、代理镜像、protoc依赖与环境变量设置,说明多版本共存、升级回退与CI流水线安装的做法,并整理安装阶段的常见报错与验证步骤。

2026.11.12

60

15

Go-Zero零基础入门教程
Go-Zero零基础入门教程

本专题整理Go-Zero的入门知识,从框架定位、环境准备到跑通第一个API服务,讲清目录分层、配置加载、日志与中间件的基本用法,并对比Gin、Kratos的差异,帮助Go开发者建立整体认知,判断它是否适合当前项目。

2026.11.12

40

15

Postman身份认证怎么配置
Postman身份认证怎么配置

本专题从实际API接口调试场景出发,讲解Postman身份认证的完整配置流程,包括常见认证协议介绍、授权参数设置、访问令牌填写、OAuth 2.0授权及认证失败排查,帮助用户正确完成接口授权与访问验证。

2026.11.06

140

12

Postman请求头Headers怎么设置
Postman请求头Headers怎么设置

围绕Postman HTTP请求头配置展开,讲解Headers选项的具体使用方式,包括Key和Value参数填写、请求头信息添加、常见Header字段设置及请求头修改方法,帮助用户解决接口调试过程中遇到的请求头配置问题。

2026.11.06

120

13

Postman接口请求发送教程
Postman接口请求发送教程

详细介绍Postman接口请求的创建与发送方法,涵盖GET和POST请求、URL配置、请求参数、Headers请求头、Body请求体设置以及响应结果查看,帮助用户掌握Postman接口请求的完整操作流程。

2026.11.06

120

10

DBeaver怎么导入数据
DBeaver怎么导入数据

DBeaver数据导入专题,整理Excel、CSV、SQL、TXT等常见文件的数据导入方法,涵盖目标数据库选择、数据表创建、字段映射、数据类型设置及导入参数配置等操作,帮助用户将本地文件中的数据写入数据库,并了解批量导入、重复数据处理和导入失败等常见问题的解决方法。

2026.11.05

100

12

DBeaver安全防护教程大全
DBeaver安全防护教程大全

DBeaver安全防护专题,围绕数据库连接安全、账号密码保护和访问权限管理展开,整理SSL/TLS加密连接、SSH隧道、数据库凭据保存及证书验证等配置方法,同时介绍敏感信息保护、远程连接风险和高权限账号使用等注意事项,帮助用户提高数据库日常管理的安全性。

2026.11.05

80

12

DBeaver连接创建与配置教程
DBeaver连接创建与配置教程

本专题聚焦DBeaver数据库连接配置与管理,覆盖从新建连接、选择数据库类型到填写连接参数和验证连接的完整流程。针对MySQL、PostgreSQL、SQL Server等常见数据库,整理相应的连接设置方法,并提供驱动缺失、认证失败和连接异常等问题的排查方向。

2026.11.04

100

16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
WebStorm 官方调试文档
WebStorm 官方调试文档

共0课时 | 0人学习

React 教程
React 教程

共58课时 | 12.5万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 6.8万人学习