引言
JavaScript采用回调函数来处理异步编程,但是用户经常会陷入一种被称作回调金字塔(Pyramid of Doom)的困境,即在回调函数内部嵌套其它回调函数,层层嵌套使得代码清晰性急剧下降。
1 | asyncOperation(function(data){ |
defer和promise是jQuery实现中,最难也最有趣的部分,它们以优雅的方式实现了JS世界中的异步回调。
1 | promiseSomething() |
基础知识
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 | jQuery.Callbacks = function( options ) { |
其核心方法为fire, 定义了依次触发列表中每个回调函数的逻辑:
1 |
|
核心的代码是list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] , 定义了如何触发回调函数。
作为一个列表,Callbacks object也定义了相应的add 和 remove方法,用来添加或删除某个回调函数。
初始化Callbacks object的时候,可以传入一个option字符串,具体可参考。jQuery API 文档
deferred.js
阅读jQuery.Deferred对象的定义,首先发现的有趣地方在于,其定义了一个promise方法,返回一个扩展的promise对象:
1 | Deferred: function( func ) { |
对于内部的promise对象来说,最核心的是then方法:
1 | promise = { |
按照Promises/A规范的定义,then 方法可以接受 3 个函数作为参数。前两个函数对应 promise 的两种状态 fulfilled 和 rejected 的回调函数。第三个函数用于处理进度信息。
then方法内部,最核心的定义是resolve方法,用来计算Promise对象代理的真实数据,实现状态的变迁, 并触发相应的回调函数handler。
1 | function resolve( depth, deferred, handler, special ) { |
实际的Promise对象,其代理的真实数据也可能是一个Promise对象。resolove方法的有趣之处在于通过递归的方式解析层层嵌套的Promise对象:
1 | returned = handler.apply( that, args ); |
读完deferred.js的实现,试着解答困惑自己的一些问题:
-
[Q] Promise对象如何计算其代理的真实数据对象?
-
[A] 当客户端代码调用
then或者done方法时,底层会调用Promise对象的resolve方法,计算其代理的真实数据对象并实现状态变迁,根据对象的不同状态调用相应的fulfil,reject或progress函数。 -
[Q] 何时触发该计算?
-
[A] 解析
Promise对象代理的数据是一个异步的操作,可以看到window.setTimeout( process );这样的代码,表明由JS解析器选择合适的时机来触发process。 -
[Q] 回调链是如何构建的?
-
[A] 在
then方法实现的最后,我们可以看到代码return jQuery.Deferred(...).promise();, 证明其返回的仍然是一个Promise对象,于是可以继续添加.then调用,构成回调链。
ajax.js
理解了defer和promise, 再来看ajax.js的实现,就会变得十分简单。核心的定义是$.ajax方法:
1 | jQuery.extend( { |
ajax底层调用的是JS中的XHR(XmlHttpRequest),所以我们在代码中可以看到一个对应的jqXHR对象:
1 | // Fake xhr |
为了保证异步,jqXHR被实现成一个Promise对象:
1 |
|
我们可以在后续代码中看到jqXHR对象上绑定的回调函数:
1 | // Install callbacks on deferreds |
其余代码涉及ajax通信的琐碎细节,在此不再赘述。