jQuery 源码精粹4 -- defer与promise

引言

JavaScript采用回调函数来处理异步编程,但是用户经常会陷入一种被称作回调金字塔(Pyramid of Doom))的困境,即在回调函数内部嵌套其它回调函数,层层嵌套使得代码清晰性急剧下降。

1
2
3
4
5
6
asyncOperation(function(data){
anotherAsync(function(data2){
yetAnotherAsync(function(){
});
});
});

defer和promise是jQuery实现中,最难也最有趣的部分,它们以优雅的方式实现了JS世界中的异步回调。

1
2
3
4
5
6
7
8
9
promiseSomething()
.then(function(data){
return anotherAsync();
})
.then(function(data2){
return yetAnotherAsync();
})
.then(function(){
});

基础知识

Defer和Promise

一个 Promise 对象代表一个目前还不可用,但是在未来的某个时间点可以被解析的值。

类似于代理模式,Promise对象扮演真实数据对象的代理。Defer对象又对Promise做了一层封装,向用户屏蔽了Promise对象的内部私有方法。

通过给调用直接返回一个Defer对象,用户代码不必阻塞等待结果,可以继续向下执行。Defer和Promise使得用户可以用同步的方式编写异步代码,同步为表,异步为里,兼具同步代码的清晰性和异步代码的高效性。

许多其它语言也提供了对Defer和Promise的支持。

  • 函数式编程是Defer和Promise的起源,函数式世界里通常称作Future和Promise,可以在Scala, Erlang 及 Cloure看到相应实现。
  • 著名的Python twisted库,也有自己的Defer实现

回调函数与回调链

Promise对象可以有三种状态:

  • 未完成 (unfulfilled)
  • 完成 (fulfilled)
  • 失败 (failed)

Promise 的状态只能由未完成转换成完成,或者未完成转换成失败

可以在Promise 对象上绑定一串回调函数,构成一个回调函数链。一旦真实数据变得可用,基于当前Promise对象的状态,调用相应的回调函数。

代码分析

callbacks.js

jQuery.Callbacks对象本质上维护了一个回调列表:

1
2
3
4
5
6
jQuery.Callbacks = function( options ) {
...
// Actual callback list
list = [],
...
}

其核心方法为fire, 定义了依次触发列表中每个回调函数的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// Fire callbacks
fire = function() {

// Execute callbacks for all pending executions,
// respecting firingIndex overrides and runtime changes
fired = firing = true;
for ( ; queue.length; firingIndex = -1 ) {
memory = queue.shift();
while ( ++firingIndex < list.length ) {

// Run callback and check for early termination
if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false &&
options.stopOnFalse ) {

// Jump to end and forget the data so .add doesn't re-fire
firingIndex = list.length;
memory = false;
}
}
}

核心的代码是list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ], 定义了如何触发回调函数。

作为一个列表,Callbacks object也定义了相应的addremove方法,用来添加或删除某个回调函数。

初始化Callbacks object的时候,可以传入一个option字符串,具体可参考。jQuery API 文档

deferred.js

阅读jQuery.Deferred对象的定义,首先发现的有趣地方在于,其定义了一个promise方法,返回一个扩展的promise对象:

1
2
3
4
5
6
7
Deferred: function( func ) {
// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
return obj != null ? jQuery.extend( obj, promise ) : promise;
}
}

对于内部的promise对象来说,最核心的是then方法:

1
2
3
4
5
promise = {
then: function( onFulfilled, onRejected, onProgress ) {
...
}
}

按照Promises/A规范的定义,then 方法可以接受 3 个函数作为参数。前两个函数对应 promise 的两种状态 fulfilled 和 rejected 的回调函数。第三个函数用于处理进度信息。

then方法内部,最核心的定义是resolve方法,用来计算Promise对象代理的真实数据,实现状态的变迁, 并触发相应的回调函数handler

1
2
3
function resolve( depth, deferred, handler, special ) {
...
}

实际的Promise对象,其代理的真实数据也可能是一个Promise对象。resolove方法的有趣之处在于通过递归的方式解析层层嵌套的Promise对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
returned = handler.apply( that, args );

then = returned && ( typeof returned === "object" || typeof returned === "function" ) && returned.then;

if ( jQuery.isFunction( then ) ) {
maxDepth++;

then.call(
returned,
resolve( maxDepth, deferred, Identity, special ),
resolve( maxDepth, deferred, Thrower, special ),
resolve( maxDepth, deferred, Identity, deferred.notifyWith )
);
}

读完deferred.js的实现,试着解答困惑自己的一些问题:

  • [Q] Promise对象如何计算其代理的真实数据对象?
  • [A] 当客户端代码调用then或者done方法时,底层会调用Promise对象的resolve方法,计算其代理的真实数据对象并实现状态变迁,根据对象的不同状态调用相应的fulfil, rejectprogress函数。

  • [Q] 何时触发该计算?

  • [A] 解析Promise对象代理的数据是一个异步的操作,可以看到window.setTimeout( process ); 这样的代码,表明由JS解析器选择合适的时机来触发process

  • [Q] 回调链是如何构建的?

  • [A] 在then方法实现的最后,我们可以看到代码return jQuery.Deferred(...).promise();, 证明其返回的仍然是一个Promise对象,于是可以继续添加.then调用,构成回调链。

ajax.js

理解了deferpromise, 再来看ajax.js的实现,就会变得十分简单。核心的定义是$.ajax方法:

1
2
3
4
5
6
7
jQuery.extend( {

// Main method
ajax: function( url, options ) {
...
}
})

ajax底层调用的是JS中的XHR(XmlHttpRequest),所以我们在代码中可以看到一个对应的jqXHR对象:

1
2
3
4
5
// Fake xhr
jqXHR = {
readyState: 0,
...
}

为了保证异步,jqXHR被实现成一个Promise对象:

1
2
3

// Attach deferreds
deferred.promise( jqXHR );

我们可以在后续代码中看到jqXHR对象上绑定的回调函数:

1
2
3
4
// Install callbacks on deferreds
completeDeferred.add( s.complete );
jqXHR.done( s.success );
jqXHR.fail( s.error );

其余代码涉及ajax通信的琐碎细节,在此不再赘述。

参考文档

  1. deferred对象详解

  2. Promise 的工作原理