深层 jQuery 特性:Deffered, Promise 和事件

先看一下大神的教程:http://www.ruanyifeng.com/blog/2011/08/a_detailed_explanation_of_jquery_deferred_object.html

不过其实事情并没有那么复杂,下面由我来用最简单实用的方式解释这个东西。

一、Deffered 机制是用来做什么的

Deffered 是一种设计模式,用于从逻辑上改善 javascript 的回调和同步机制。

万恶的旧世界

如大家所知,javascript 是一种具备特殊性质的语言,它是单线程的,通过时间循环来运作的语言。

于是,javascript 不能像其他语言(例如 java 或者 python 等等)一样,产生多个线程,或者产生中断,以及等待其他线程的完成进行同步等。

于是,我们很多时候,需要通过回调函数来进行一些类似与线程同步的逻辑。

例如,我们希望在一个 Ajax 请求完成和失败之后做相应的事情,我们要这样写:

$.get(url, function(data) {
    // Callback Done
}, function(jqXHR) {
    // Callback Fail
});

这样,对于一些不是马上执行的操作,我们希望它完成之后执行某个操作,在一个最直接的编码方法上说,我们可以将回调函数作为参数作为参数传入:

var delayAction = function(delay, callback) {
    // delay is the timeout in microseconds
    setTimeout(function() {
        var return_value = doAction();
        console.log('action is done');
        callback(return_value);
    }, delay);
}

大概是这个样子,我们初学 javascript 大抵也是这样写的。

二、旧世界的弊端

然而,这种写法绝对称不上好,哪里有问题?

第一:不规范

对于所有的回调,基本上是每次写都是随意的,可以调一个,两个,三个,名称各异,编码质量不容乐观;

第二:嵌套的问题

嵌套的时候,如果涉及多级的回调,这个本来应当是流水型的逻辑就变成了一个很深的堆栈,代码不利于理解和阅读;

第三:如果需要多个操作进行同步,无计可施

如果我们有两个或者多个 delayAction,也不知道其完成的顺序先后,我们需要在所有异步操作完成之后,执行某一个回调,几乎难以实现。

三、新世界的端倪

Promise A+ 标准,或者是说设计模式,正是为了打破旧世界而产生的:

Promise A+ 的主页:https://promisesaplus.com/

Promise A+ 事实上已经是一种设计模式以及一个设计标准,在不同的库里面都不同地实现了这个标准,jQuery 里面就有。

事实上,在 jQuery 里面,这种设计正已深深扎根,看我的另一篇文章:

JQUERY – ASYNC OR SYNC 异步与同步深层剖析

jQuery 里面的 Ajax 方法,正是应用了 Promise 机制。

我们在调用 $.ajax 系列方法之后,这个请求就发出去了,然后直接返回一个 jqXHR 对象,外层函数就跑过去了。

如果 $.ajax 的调用里面指定了回调函数,那么,在 ajax 完成或者失败回来之后,对回调函数的调用就会插到事件循环里面并被排队执行。

当然,Promise 模式当然不是这样来进行回调,而是其返回的 jqXHR 对象,官方文档里面说到,jQuery 1.5 之后,jqXHR 就是一个 Promise 对象,我们可以在上面通过 done/fail/always/then 等方法来指定回调:

$.ajax(url).done(function(data) {
    // Done callback
}).fail(function(xhr) {
    // Fail callback
}); // 后面还可以像挂车厢一样一节一节挂下去。

这系列的 done/fail/always/then 方法,并非 jqXHR 独有,而是 promise 对象的接口。

四、promise 的并联(相当于进程同步)

而且,我们还可以收集一系列的 Promise,让他们全部完成之后(通过$.when方法),再执行回调,相当于进程的同步:

var xhr1 = $.get(url1);
var xhr2 = $.get(url2);
var xhr3 = $.get(url3);
var xhrAll = $.when(xhr1, xhr2, xhr3).done(function() {
    // All done callback.
});

又或者用 apply 方式来进行不定个 jqXHR(promise) 的收集:http://stackoverflow.com/q/24050866/2544762

var xhrs = [xhr1, xhr2, xhr3/* , ... */];
var xhrAll = $.when.apply($, xhrs).done(function() {
    // All done callback.
});

五、promise 的串联

这就是 promise 的一系列妙用,$.when 方法可以看作是 promise 对象的并联,当然,我们还有串联,先举个反例:

$.get(url_userinfo).done(function(userdata) {
    $.get(url_permissions, {user: userdata.id}).done(function(permissions) {
        if('read_contacts' in permissions) {
            $.get(url_contacts, {user: userdata.id}).done(function(contacts) {
                console.log(contacts);
            });
        }
    });
});

如是,我们如果有一个链条的 promise,每一个都要求上一个完成之后再触发下一个,在上面这种写法,就免不了回调。

这个正是在使用 Promise 的时候容易陷入的反面模式 (deferred antipattern):http://stackoverflow.com/q/23803743/2544762

事实上,在 Promise A+ 的设计规范里面,如果在一个 promise 的回调内部,return 另一个内部产生的 promise 对象,那么外部的 promise 对象就会将这个内部的 promise 对象串联起来,并且等待直到内部的 promise 完成,于是,上面的这段代码就应当写成:

var xhrAll = $.get(url_userinfo).done(function(userdata) {
    return $.get(url_permissions, {user: userdata.id});
}).done(function(permissions) {
    if('read_contacts' in permissions) {
        return $.get(url_contacts, {user: userdata.id});
    }
}).done(function(contacts) {
    console.log(contacts);
});

明显可以看到上述用标准 promise 模式改写之后代码的好处:

  1. 成功将多层嵌套的代码变成一层的链式调用,也像挂车厢一样,代码质量提升了;
  2. 我们可以从最外层获取到整个异步调用链最终完成的 promise,这在 antipattern 的 代码里面根本无法做到;
  3. 我们可以从最外层返回的 xhrAll 捕获整个链条上面任何环节产生的 fail,并且统一进行处理。

六、双链机制

了解了上面的一系列机制,我们可以将一系列的 Deferred (或 Promise) 对象通过串联或者并联,像电路一样,来获取一个最终的 Promise 对象,对于每个中间的 Promise 对象,实际的执行调用就像一条链条,每个环节都可能产生 done 或者 fail 的成功或这失败的状态,这些中间状态顺着 deferred 链的传导,最终会影响整体 Deferred 对象的状态。

并联情况

假如是并联的情况(使用 $.when 传入多个 promise),全部 promise 成功 resolve 之后,整体就 resolve,只要一个被 reject,整体就会 reject。

注意 $.when 如果有一个 reject,那么剩下的 promise 不管完不完成,整体 promise 马上触发,其参数就是 reject 产生的参数;

如果全部都 resolve 了,那么整体的 promise 随即触发,参数为传入的 promise 各自的参数,每个作为一个数组。

串联情况

这是一个最有意思的情况,每一级的 deferred 都可以根据其 done/fail 回调函数的返回值决定后面是另一个串接的 deferred 或者是结束。

如果内部的 deferred 的 done 被触发,我们返回另一个 deferred,那么后面的 deferred 将会变成最终的 deferred 回调。

如果我们并没有返回 deferred 类型,那么这个 deferred 链就会结束。

同样的,我们在 fail 上面也可以返回一个 deferred 对象,这个时候,这个状态就会从这个错误节点重新被串联回正常处理的链条继续执行。

于是,我们的整个 deferred 对象应该构成一个 DAG(有向无环图),并且对于 resolve 和 reject 的清情况应该是分别处理的,那么当程序运行到某一个没有出边的 deferred 调用的时候,这个 promise 对象就会被传到最外层,并且触发对应的状态。

七、定义我们自己的 Deferred

关于 Deferred 和 Promise 对象的解释,请直接看文章顶部阮老师的博文,下面我们只放出一招,永远只用这一招即可:

这是一个涉及了上述所有环节的样例(包括串联和并联),请自行截取适当的片段使用:

var promises = [];
for(var delay = 1000; delay < 4000; delay += 1000) {
    var promise = $.Deferred(function(dfd) {
        setTimeout(function() {
            var score = Math.random();
            if(score > 0.5) {
                console.log('delay success: ' + delay);
                dfd.resolveWith(this, [delay]);
                // Without arguments:
                // dfd.resolve();
            } else {
                console.log('delay fail: ' + delay);
                dfd.rejectWith(this, [delay, score]);
                // Or:
                // dfd.reject();
            }
        }, delay);
    }).promise();
    promises.push(promise);
}

$.when.apply($, promises).done(function(delay) {
    console.log('All steps done.');
    // Cascading
    return $.get('https://www.huangwenchao.com.cn').done(function(data) {
        alert('Final done: length = ' + data.length);
    });
}).fail(function(delay, score) {
    console.log('Some step failed.');
    console.log('score = ' + score);
});

【转载请附】愿以此功德,回向 >>

原文链接:https://www.huangwenchao.com.cn/2015/09/jquery-deffered-promise.html【深层 jQuery 特性:Deffered, Promise 和事件】

发表评论

电子邮件地址不会被公开。 必填项已用*标注