jQuery源码分析(九): 异步机制

前端语法/样式/布局 2016-11-06

起步

js编程常常有各种异步的处理,比如远程获取数据。js是单线程的,所以完成异步往往需要借助浏览器事件驱动。jQuery就提供了一个抽象的 非阻塞 解决方案: Deferred

$.Deferred()

$.Deferred在jQuery代码内部有四个模块被使用,分别是promise方法、 DOM readyAjax模块及动画模块。jq1.5版本后的$.ajax()返回的不是XHR对象了,而是经过包装的Deferred对象,具有promise规范。

Deferred提供一种方法来执行一个或多个对象的回调函数:

$.when($.ajax("a1.html"), $.ajax("a2.html"))
  .done(function(){ alert('2次回调都正确返回了') })
  .fail(function(){ alert('出错了'); });

这段代码的意思是:先执行两个操作$.ajax("a1.html")和$.ajax("a2.html"),如果都成功了,就运行done()指定的回调函数;如果有一个失败或都失败了,就执行fail()指定的回调函数。

源码分析

Deferred的代码在3200多行开始,紧跟在Callbacks后面,据说是1.7版本后回调函数Callbacks从Deferred中抽离出去了。

大多情况下,promise作为一个模型,提供了描述延时(或将来)概念的解决方案。Promise/A是一种规范,而Deferred可以看作这种规范的具体实现,旨在提供通用的接口,用来简化异步编程难度。

promise有三种状态:未完成(unfulfilled)已完成(resolved)拒绝(rejected)。对应了Deferred中的:

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") ]
],

这些也可以看做是 观察者模式 的一种实现。

//代码有省略
jQuery.extend({
   Deferred:function(func){
        var tuples = [
            //1 动作
            //2 侦听器
            //3 最终状态
            //后面的操作将是围绕这些接口处理
            ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"],
            ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"],
            ["notify", "progress", jQuery.Callbacks("memory")]
          ],
          //deferred的状态,三种:pending(初始状态), resolved(解决状态), rejected(拒绝状态)
      //其实就是tuples最后定义的
          state = "pending",
          //内部promise对象,作用:
          //1:通过promise.promise( deferred );混入到deferred中使用
          //2:可以生成一个受限的deferred对象,
          //   不在拥有resolve(With), reject(With), notify(With)这些能改变deferred对象状态并且执行callbacklist的方法了
          //   换句话只能读,不能改变了
          //扩展
          //  done fail pipe process 
          promise = {
            state: function() {},
            always: function() {},
            then: function( /* fnDone, fnFail, fnProgress */ ) {},
            promise: function(obj) {}
          },
          deferred = {};
          //管道接口,API别名
          promise.pipe = promise.then;
          //逐个添加所有的接口到deferred对象上
          jQuery.each(tuples, function(i, tuple) {
            //代码省略
          });
          //转成成promise对象
          promise.promise(deferred);
          //如果传递的参数是函数,直接运行
          if (func) {
            func.call(deferred, deferred);
          }
          return deferred;
   },

   when:function(func){
      ...省略代码....
      return deferred.promise();
   }
})

显而易见,$.Deferred()是个工厂类,返回的其实是内部的deferred对象。tuples 创建三个$.Callbacks对象,分别表示成功,失败,处理中三种状态;tuples 创建三个$.Callbacks对象,分别表示成功失败处理中三种状态;扩展primise对象生成最终的Deferred对象,返回该对象。

分析

可以看到Deferred的实现是严重依赖 $.Callbacks 对象的,存储deferred依赖的数据,done、fail、progress就是jQuery.Callbacks("once memory"):

var list = jQuery.Callbacks("once memory")
promise['done'] = list.add;

tuples 元素集 其实是把相同有共同特性的代码的给合并成一种结构,然后通过一次处理:

jQuery.each( tuples, function( i, tuple ) {
    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;
});

将回调函数保存一个引用:promise[ tuple[1] ] = list.add;这样等价于:

promise.done         = $.Callbacks("once memory").add;
promise.fail         = $.Callbacks("once memory").add;
promise.progress     = $.Callbacks("memory").add;

如果存在deferred最终状态:stateString = tuple[ 3 ],默认会预先向doneList,failList中的list添加三个回调函数:

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 );
}

其中 i^1 进行异或运算,当i = 0,索引取的是1,所以它其实是把索引0 1对调,取得值分别为 failList.disabledoneList.disable 。 通过stateString有值这个条件,预先向doneList,failList中的list添加三个回调函数,分别是:

doneList : [changeState, failList.disable, processList.lock]
failList : [changeState, doneList.disable, processList.lock]
  • changeState 改变状态的匿名函数,deferred的状态,分为三种:pending(初始状态), resolved(解决状态), rejected(拒绝状态)
  • 不论deferred对象最终是resolve(还是reject),在首先改变对象状态之后,都会disable另一个函数列表failList(或者doneList)
  • 然后lock processList保持其状态,最后执行剩下的之前done(或者fail)进来的回调函数

总的来说,这个each循环一共做了这几件事: 第一步是done/fail/引用list.add也就是callbacks.add; 第二步是将回调函数changeState, failList.disable, processList.lock存入回调对象中; 第三步deferred对象扩充6个方法,resolve/reject/notifycallbacks.fireWith,执行回调函数,resolveWith/rejectWith/notifyWithcallbacks.fireWith 队列方法引用.

最后让deferred注入promise接口:promise.promise( deferred );

promise: function( obj ) {
    return obj != null ? jQuery.extend( obj, promise ) : promise;
}

有此可见,每次调用$.Deferred()返回都是一个新的对象,这和Callbacks()的做法一致。

then的实现

then的返回是另一个新的异步模型对象,上文有说它的别名是管道:promise.pipe = promise.then;因此它内部需要保存自己的状态和各自的处理方法,通过这个方法把所有对象操作都串联起来。这个方法就是then:

then: function( /* fnDone, fnFail, fnProgress */ ) {
    var fns = arguments;
    return jQuery.Deferred(function( newDefer ) {
        jQuery.each( tuples, function( i, tuple ) {
            //取出参数
            var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
            // deferred[ done | fail | progress ] for forwarding actions to newDefer
            // 添加done fail progress的处理方法
            // 针对延时对象直接做了处理
            deferred[ tuple[1] ](function() {
                var returned = fn && fn.apply( this, arguments );
                if ( returned && jQuery.isFunction( returned.promise ) ) {
                    returned.promise()
                        .done( newDefer.resolve )
                        .fail( newDefer.reject )
                        .progress( newDefer.notify );
                } else {
                    newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
                }
            });
        });
        fns = null;
    }).promise();
},

其实在内部创建了新的Deferred对象,参数是newDefer。Deferred内部就是为了改变下上下文this为deferred,然后传递deferred给这个回调函数了,所以 newDefer 就指向内部的deferred对象了。这是怎么回事呢?

可以看到Deferred传了一个参数jQuery.Deferred(function( newDefer ){}),一个func,根据:

// Call given func if any
if ( func ) {
    func.call( deferred, deferred );
}

所以newDefer可以看做是:

newDefer = $.Deferred();

那么func回调的处理的就是过滤函数了

deferred[ tuple[1] ](function() {
    var returned = fn && fn.apply( this, arguments );
    if ( returned && jQuery.isFunction( returned.promise ) ) {
        returned.promise()
            .done( newDefer.resolve )
            .fail( newDefer.reject )
            .progress( newDefer.notify );
    } else {
        newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
    }
});

deferred变量按作用域查找是上一层的元素,或者说是父deferred,这里这段代码也有编译函数的概念,讲未来要执行的代码,预先通过闭包函数也保存起来,使其访问各自的作用域.

总的来讲then一共做了几件事: 第一,分解tuples元素集:

jQuery.each( tuples, function( i, tuple ) {
//过滤函数第一步处理
}

第二,分别为deferred[ done | fail | progress ]执行对应的add方法,增加过滤函数给done | fail | progress 方法

deferred[ tuple[1] ](
传入过滤函数
)//过滤函数 执行的时候在分解

可以看成

deferred[done] = list.add = callback.add;

第三步,返回return jQuery.Deferred().promise()此时构建了一个新的Deferred对象,但是返回的的是经过promise()方法处理后的,返回的是一个受限的promise对象,给父deferred对象的[ done | fail | progress ]方法都增加一个过滤函数的方法. deferred其实就是根级父对象的引用,所以就嵌套再深,其实都是调用了父对象deferred[ done | fail | progress 执行add

总结

这部分好难理解,很多嵌套,自己慢慢折腾代码吧。


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!