Light
  • 首页
  • 关于

2016-03-11

浅谈jQuery中的defer对象

前段时间在网易的笔试题中遇到了用原生JS实现一个promise对象,之前只对promise有过概念上的了解,但从来没去考虑过如何实现,所以没有答上来,后来去找了一些promise对象的一些实现思路,又看了下jQuery中对defer对象的实现,感觉收获挺大,在这里做个简单总结。

一.关于Promise规范

在了解jQuery如何实现defer对象之前,我们首先要对Promise规范有所了解。Promise作为CommonJS的规范之一,主要为了帮助前端开发人员更好地控制代码的流程,避免回调函数的多层嵌套。

为什么使用Promise规范?

我们在异步请求中经常会遇到这样的情况,存在两个异步请求,其中第二个请求依赖于第一个请求获取的数据,以下是采用传统回调函数的书写方式:

$.ajax({
    url: url1,
    success: function(res){
        $.ajax({
            url: url2,
            success: function(res){
                ...
            }
        })
    }    
})

我们不得不将第二个请求写到第一个请求的成功回调中,这样的嵌套降低了代码结构的可读性,尤其是在请求链很长的情况下,这样的代码很容易造成开发人员的困惑。

而在Promise规范下,这样的请求一般会被书写成如下形式:

var defered = $.ajax({url: url1});
defered.then(function(){
    ...
})

值得一提的是,Promise对象在调用then方法后一般会返回一个新的Promise对象,你可以在之后继续调用相同的方法,使其按序执行,这样的链式写法会更具逻辑性.

如何实现一个简单的Promise?

Promise只是一种规范,虽然各种各样的库对它的实现都不一样,但它们都还是包含一些基本的元素:
1.state: 代表当前执行状态,一般有pending(等待中),resolved(已完成),rejected(已拒绝)
2.done,fail,then: 添加相应状态下的执行函数,done对应resolved,fail对应rejected
3.resolve,reject: 更改状态的函数
在了解这些基本元素后,我们可以考虑用原生JS实现一个简单的Promise对象

var Promise = function() {
    this.state = "pending"; //promise状态
    this.list = []; //函数列表
}

Promise.prototype = {

    constructor: Promise,

    resolve: function(){
        this.state = "resolved";
        for(var i = 0, len = this.list.length; i < len; i++) {
            this.list[0].call(this);
            this.list.shift();
        }
    },

    then: function(func){
        if(typeof func == "function") {
            this.list.push(func);
        }
    }
}

这是一个极其简化的Promise,其中只实现了resolve和then两种方法,事实上这就是Promise规范的核心流程,其他像done,fail,reject等等只是对应的状态不同而已,原理还是一样的。
那么,我们该如何运用这个Promise对象呢?

var promise = new Promise();

setTimeout(function(){

    promise.resolve();

},2000);

promise.then(function(){
    console.log("success!");
});    

在上述代码中,我们通过then方法将一个匿名函数push到该promise对象的函数列表中,并用setTimeout构造了一个简单的异步场景,在页面上等待2秒后,可以看到控制台打印出success,说明then方法中传的函数得到了调用,这就是promise的执行流程,通过then方法给函数队列添加函数,再通过resolve从函数队列中取出函数执行并更改状态。

二.jQuery的$.Callback对象

在jQuery中,对defer对象的实现完全依赖于它的Callback对象,所以在解读defer对象之前,我们必须先了解它的Callback对象。

jQuery构造了一个Callback对象来进行回调函数的统一管理,Callback对象维护一个函数队列,并提供一些外部的方法来操作该队列中的函数,一般来说,我们在外部使用的时候很少会用到Callback对象,然而jQuery内部不少地方都用到了该对象,首先我们先来看看该对象的用法。

function a(){
    console.log("callback");
}
var cb = $.Callbacks();
cb.add(a);
setTimeout(function(){
    cb.fire();
}, 2000)

在页面中执行上述代码,可以看到2秒后控制台打印出callback,可以看出,这个流程和我们上述介绍的Promise规范的实现极其相似,只是这次我们通过add添加函数,通过fire来执行。

我们可以看看Callback对象在jQuery的实现部分,以下代码摘自jQuery-2.0.3版本

jQuery.Callbacks = function( options ) {

    options = typeof options === "string" ?
        ( optionsCache[ options ] || createOptions( options ) ) :
        jQuery.extend( {}, options );

    var 
        memory,
        fired,
        firing,
        firingStart,
        firingLength,
        firingIndex,
        list = [],    
        stack = !options.once && [],
        fire = function( data ) {
            ...        
        },
        self = {
            add: function() {
                ...
            },
            remove: function() {
                ...
            },
            has: function( fn ) {
                ...
            },
            ...
            fireWith: function( context, args ) {
                ...
            },
            fire: function() {
                ...
            },
            ...
        };

    return self;
};

我们先省略中间函数的逻辑实现部分,单单从该对象的结构来看,可以看出这其实是一个闭包结构,它定义了像memory,fired等等内部变量,返回一个self对象,而该self对象包含各种各样的方法来操作上面定义的内部变量,其中就包括我们在上文使用的add和fire。

那么首先,我们可以看看关于add方法的实现逻辑

add: function() {
    if ( list ) {
        ...    
        (function add( args ) {
            jQuery.each( args, function( _, arg ) {
                var type = jQuery.type( arg );
                if ( type === "function" ) {
                    if ( !options.unique || !self.has( arg ) ) {
                        list.push( arg );
                    }
                } else if ( arg && arg.length && type !== "string" ) {
                    add( arg );
                }
            });
        })( arguments );
        ...
    }
    return this;
},

主要逻辑是中间的这段匿名函数自执行,它将add方法的arguments对象作为参数传入,然后对其进行遍历,判断其类型是否为函数,如果是就将其push到函数队列中,其中unique作为该对象的一个option主要是为了控制队列中的每个函数是否应该独一无二。
另外值得注意的是参数类型不是函数的情况,从else if的表达式中可以看出,参数拥有length属性并且不是字符串,那么意味着该参数只可能是数组,这时候递归调用add并将该参数传入,也就是说add方法还可以这样使用:

var cb = $.Callbacks();
cb.add([a,b]);

这么做可以同时将a,b函数加入到函数队列中
再了解了add方法的实现后,我们可以再来看看fire方法是如何实现的

fireWith: function( context, args ) {
    if ( list && ( !fired || stack ) ) {
        ...
        if ( firing ) {
            stack.push( args );
        } else {
            fire( args );
        }
    }
    return this;
},
fire: function() {
    self.fireWith( this, arguments );
    return this;
},

可以看出,当我们调用fire方法时,实际上内部调用的是fireWith方法,而fireWith则又进一步调用了Callback内部的fire方法,定义如下:

fire = function( data ) {
    memory = options.memory && data;
    fired = true;
    firingIndex = firingStart || 0; //目前执行到的顺序
    firingStart = 0; 
    firingLength = list.length; //函数队列的长度
    firing = true;    //相当于锁,fire期间不允许再重复fire
    //遍历队列并进行函数调用
    for ( ; list && firingIndex < firingLength; firingIndex++ ) {
        if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
            memory = false;
            break;
        }
    }
    firing = false; //解锁
    ...
},

主要逻辑在中间的for循环,它从firingIndex指定位置遍历了目前的函数队列,并通过apply调用函数,stopOnFalse作为选项为真时,一旦函数返回false就跳出循环。

以上是jQuery对add和fire的实现逻辑,当然Callback对象还有各种各样的选项和方法,在这里由于篇幅有限就不再赘述。

jQuery中defer对象的实现

在了解上述部分后,我们就可以正式解读defer对象的实现部分

事实上,jQuery的defer对象就是由callback对象所生成的实例,我们可以在defer对象的构造器中找到这样的代码:

var 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") ]
        ],
        state = "pending",
        promise = {
            state: {
                ...
            },
            always: {
                ...
            }                        

jQuery通过遍历tuples数组来生成不同状态的promise对象,其代码如下:

jQuery.each( tuples, function( i, tuple ) {
        //list: 回调对象
        var list = tuple[ 2 ],
            stateString = tuple[ 3 ];

        // promise[ done | fail | progress ] = list.add
        promise[ tuple[1] ] = list.add;

        // Handle state
        // 执行完方法后处理状态
        if ( stateString ) {
            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 );
        }

        // deferred[ resolve | reject | notify ]
        deferred[ tuple[0] ] = function() {
            deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
            return this;
        };
        deferred[ tuple[0] + "With" ] = list.fireWith;
    });

可以看出,对于resolved和rejected等几种不同的状态有着不同的callback对象,比如resloved状态的resolve方法指向其对应callback对象的fireWith方法,而done方法则指向callback对象的add方法,构造器最后返回该defer对象,以供我们进行调用。

本文简要描述了jQuery defer对象的原理,当然,关于defer对象还有很多巧妙的细节,由于篇幅原因不再描述,我会继续对其进行探索。