jQuery Deferred API(上)
发布在读懂jQuery2014年9月27日view:6457
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

聊完了Callback API,接下来我们来聊聊Deferred API吧。

一、什么是Deferred API?

按个人的理解,Deferred API是用来实现一下功能的:某个事件的执行过程是异步的,需要等待一段时间,这时我们可以把这个事件称作延迟事件,我们用一个Deferred对象来表示它,为它添加成功、失败以及正在处理中的事件处理方法,当事件成功或者失败后,调用对应的处理方法。比如要通过AJAX获取一段内容,这个过程需要一定时间,在这段时间内我们不应该阻塞后面代码的执行,而是返回一个Deferred对象,为它添加成功与失败的回调函数,然后让代码继续执行。如果AJAX调用成功,则执行成功的回调函数,比如改变界面显示;如果AJAX调用失败,则调用失败的回调函数,比如提示用户获取内容失败。

二、谁在用Deferred API?

在jQuery内部,有以下功能使用到了Deferred API:

  1. AJAX
  2. 动画
  3. DOM Ready
  4. $.fn.promise()

三、拆解Deferred API

Deferred API的代码量还是比较少的,总共才80多行,但是信息量还是挺大的,这里我们分3部分来讲解。

1. 变量们

第一部分,还是来说说内部的这些变量吧。

1. tuples

tuple,翻译成中文可以叫元组,是python的一种数据类型,与数组非常类似,区别是元组一旦定义就不可变。这个参数的定义如下:

tuples = [
    // action, add listener, listener list, final state
    [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
    [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
    [ "notify", "progress", jQuery.Callbacks("memory") ]
]

这里其实是定义了一个数组,数组的元素是数组,每个数组包含一个动作、添加回调函数的方法名、Callback对象,前两个数组还有包含一个状态的字符串。这三个数组分别代表了Deferred对象的三种状态:解决(resolve),拒绝(reject)和处理中(notify),前两者为终止状态。

这三个Callback对象,分别是这个Deferred对象成功解决时的回调列表,事件失败时的回调列表和事件执行中的执行列表。

而这三个Callback对象,分别使用了”once memory”和”memory”模式,resolve和reject状态的once模式很好理解,它们是终止状态,应该只有一次机会到达这个状态,而notify是处理中的状态,在这个状态是一段时间,可以被多次触发。而使用memory模式,则是因为当这个状态被触发后,再加入的方法应该被立即执行,并且使用触发时的参数。这里涉及到Callback API的模式,如有不明白的,请参见上一篇文章。

2. state

这个变量只是表示当前Deferred对象的状态的语句,初始值是pending。

3. promise

promise变量是一个对象,可以理解为阉割版的Deferred对象,通过它可以对Deferred对象做一部分操作,比如done(), fail()和progress(),但是不能操作改变Deferred对象状态的方法,比如resolve(),reject()等。

promise有一下几个方法,我们直接通过代码注释来理解吧。promise的then方法我会单独拎出来说,在这里就先略过了。

promise = {
    state: function() {
        // 返回Deferred对象的状态
        return state;
    },
    always: function() {
        // always方法的意义就是不管Deferred对象是resolve了还是reject了都执行这些回调
        // 也就是往两个resolve和reject这两个Callback对象都添加回调函数
        deferred.done( arguments ).fail( arguments );
        return this;
    },
    then: function( /* fnDone, fnFail, fnProgress */ ) {
        // then方法稍后再谈
    },
    promise: function( obj ) {
        // 如果传进来的参数是一个对象,则将promise对象扩展到此对象上
        // 否则返回这个promise对象
        // 吐槽下jQuery,干嘛要起同样的名字,就不能区分下吗
        return typeof obj === "object" ? jQuery.extend( obj, promise ) : promise;
    }
}
4. deferred

deferred变量就是我们返回的Deferred对象,初始值是一个空对象{}。

2. 执行逻辑

// 这里是为了向后兼容pipe方法
promise.pipe = promise.then;

// 这里遍历tuples变量,为deferred变量添加方法
jQuery.each( tuples, function( i, tuple ) {
    // list被赋值为对应的Callback对象,stateString为"resolved","rejected"或undefined
    var list = tuple[ 2 ],
        stateString = tuple[ 3 ];

    // promise[ done | fail | progress ] = list.add
    // 这一句的作用是把添加回调函数的方法放到promise对象中
    // 即promise.done()会往表示解决的Callback对象添加回调函数
    // promise.fail()会往表示失败的Callback对象添加回调函数
    // promise.progress()会往表示执行中的Callback对象添加回调函数
    promise[ tuple[1] ] = list.add;

    // 处理状态
    if ( stateString ) {
        // 对于tuples[0]和tuples[1], 他们的stateString存在
        // 因此需要给对应的Callback对象添加默认的回调:
        // 1. 修改Deferred对象状态的回调
        // 2. 调用另一个终止状态的Callback对象的disable()方法
        // 3. 调用代表处理中状态的Callback对象的lock()方法
        // 这里的前两个回调很好理解,第三个回调为什么要调用lock()方法呢?
        // 请看代码后面的解释
        list.add(function() {
            // state = [ 'resolved' | 'rejected' ]
            state = stateString;

        // [ reject_list | resolve_list ].disable; progress_list.lock
        }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
    }

    // 将三个Callback对象的fire()/fireWith()方法分别赋值给deferred对象
    // promise.resolve()会触发表示解决的Callback对象的回调函数列表
    // promise.reject()会触发表示失败的Callback对象的回调函数列表
    // promise.notify()会触发表示执行中的Callback对象的回调函数列表
    // deferred[ resolve | reject | notify ] = list.fire
    deferred[ tuple[0] ] = list.fire;
    deferred[ tuple[0] + "With" ] = list.fireWith;
});

// 将deferred对象扩展为promise对象
promise.promise( deferred );

// 如果有参数func,则执行此方法
// 在jQuery文档中,这个func的参数名是beforeStart
// 可以理解是允许我们在开始使用这个Deferred对象前做些操作
if ( func ) {
    func.call( deferred, deferred );
}

// 完成,返回deferred对象
return deferred;

关于为什么默认的三个回调里,第三个回调是调用progress_list.lock()方法,我看了网上很多的jQuery源码解读的文章,没有一篇解释为什么是lock()而不是disable()的,在Google中也搜不到相关的问题。下面是我个人的理解:

jQuery文档中有这么一段话:

Any calls to .notify() after a Deferred is resolved or rejected (or any progressCallbacks added after that) are ignored.

意思是当Deferred对象被resolve或者被reject以后,调用.notify()方法或者往代表处理中状态的Callback对象添加的回调函数都会被忽略。而调用lock()方法就有两种情况了:1. notify()方法在resolve()方法被调用前没有执行过,那么progressCallbacks会被disable掉;2. notify()方法在resolve()方法被调用前已经执行过了,那么progressCallback只会被lock住,不会被disable掉,而因为这里的Callback使用了memory模式,在resolve()之后用progress()方法添加的回调函数会被立即执行,这与jQuery的文档描述不符,所以我个人认为这是一个bug,应该使用tuples[ 2 ][ 2 ].disable而非tuples[ 2 ][ 2 ].lock。如果各位有另外的解释,还请不吝赐教。


updated: jQuery的开发者回应称文档错误,但是代码没错,就是这么设计的。还是没搞明白什么情况有这样的需求。

3. promise.then()方法

最后,我们来说说promise.then()方法吧。在1.8之前,then()方法很单纯,只是接收三种状态对应的回调函数或回调函数数组。从1.8版本开始,用promise.then()方法替代了promise.pipe()方法,它接受三个function作为参数。

新的promise.then()方法有两种情况: 1. 当传入了function时,function返回的如果是Deferred对象,则表示链式调用;否则function作为一个过滤器被使用。 2. 否则,返回一个新的promise对象,可以给这个promise对象添加三种状态的回调,这些回调会在原来的Deferred对象的某种状态被触发时相应的被调用。

第二种情况其实没什么意义,我们主要来看看第一种情况。先来看看链式调用的例子:

var request = $.ajax( url, { dataType: "json" } ),
  chained = request.then(function( data ) {
    return $.ajax( url2, { data: { user: data.userId } } );
  });

chained.done(function( data ) {
  // data retrieved from url2 as provided by the first request
});

这是jQuery官方文档给的例子,当我们调用request.then(fn)的时候,我们传进去的参数fn返回了一个jQuery的AJAX对象,它也是一个Deferred对象,那么,当这个Deferred对象完成以后,chained所添加的回调会被执行。

而当fn返回的不是Deferred对象时,则这个fn被当做filter来使用。

var defer = $.Deferred(),
filtered = defer.then(function( value ) {
    return value * 2;
  });

defer.resolve( 5 );
filtered.done(function( value ) {
  $( "p" ).html( "Value is ( 2*5 = ) 10: " + value );
});

上面的例子中,defer.then(fn)接受的参数fn是resolve的filter,它返回了一个数字,当defer被resolve的时候,filter.done()添加的回调方法会被执行,而且这些回调方法接受的参数是fn返回的值(value*2),即5*2 = 10。

接下来我们来看看promise.then()的源码吧,通过示例加源码解析,相信你能理解jQuery的用意的。

then: function( /* fnDone, fnFail, fnProgress */ ) {
    var fns = arguments;
    // 这里新建了一个Deferred对象,并返回其promise属性。
    // 在创建这个新的Deferred对象时,传入了一个方法,
    // 这个方法会在返回新的Deferred对象前被调用,
    // 并传入新建的Deferred对象作为参数
    return jQuery.Deferred(function( newDefer ) {
        // 这里对tuples进行遍历
        jQuery.each( tuples, function( i, tuple ) {
            // action = [resolve|reject|notify]
            var action = tuple[ 0 ],
                fn = fns[ i ];
            // 为deferred[ done | fail | progress ]添加回调函数
            // 这里给Deferred对象的3个Callback对象分别添加一个回调函数,
            // 这个回调函数用于触发新的Deferred对象中的回调函数:
            // 1. 如果我们传入的不是function,则将newDefer[resolve|reject|notify]
            // 加入到deferred的相应的Callback回调函数列表中
            // 2. 如果传入的function返回的不是Deferred对象,那么类似第一种情况,
            // 将newDefer[resolveWith|rejectWith|notifyWith]
            // 加入到deferred的相应的Callback回调函数列表中
            // 3. 如果传入的function返回了一个Deferred对象,那么将newDefer对象
            // 的[resolve|reject|notify]分别加入到这个返回的Deferred对象的对应的Callback列表中,
            // 这样,当Deferred对象状态改变的时候,newDefer的相应的回调函数就会被调用
            // 这种情况就适用于上面所说的链式调用AJAX请求的情况
            deferred[ tuple[1] ]( jQuery.isFunction( fn ) ?
                function() {
                    var returned = fn.apply( this, arguments );
                    if ( returned && jQuery.isFunction( returned.promise ) ) {
                        returned.promise()
                            .done( newDefer.resolve )
                            .fail( newDefer.reject )
                            .progress( newDefer.notify );
                    } else {
                        newDefer[action+"With"]( this === deferred ? newDefer : this, [returned] );
                    }
                } :
                newDefer[ action ]
            );
        });
        fns = null;
    }).promise();
}

这里只分析Deferred对象,还有Deferred helper方法$.when(),留待下回分解吧。

小广告:我的博客 YuuuuC.me #^_^#

评论
发表评论
6年前

@琅琊丶Janking 很高兴能帮到你,共勉!

6年前

刚好jQuery源码读到这,你帮了我很大忙啊!

6年前

@小黑 O(∩_∩)O哈哈~ 多谢鼓励

6年前

先收藏,JQ学习必备

WRITTEN BY
YuC_C
陈胜后裔吗?博客:lovecicy.com
TA的新浪微博
PUBLISHED IN
读懂jQuery

用了这么久jQuery了,想看看到底jQuery怎么写的,为什么这么牛呢。本栏解读的版本为1.8.1,本人(小Y)才疏学浅,如有疏漏、错误之处,还望各位大牛雅正。个人博客:YuuuuC

我的收藏