读jQuery源码 - Deferred

时间:2023-03-08 17:30:34

Deferred首次出现在jQuery 1.5中,在jQuery 1.8之后被改写,它的出现抹平了javascript中的大量回调产生的金字塔,提供了异步编程的能力,它主要服役于jQuery.ajax。

Deferred就是让一组函数在合适的时机执行,在成功时候执行成功的函数系列,在失败的时候执行失败的函数系列,这就是Deferred的本质。简单的说,模型上可以规划为两个数组来承接不同状态的函数——数组resolve里的函数列表在成功状态下触发,reject中的函数在失败状态下触发。

本文原创于linkFly原文地址

这篇文章主要分为以下知识,和上一篇博文《读jQuery源码之 - Callbacks》关联。

什么是Deferred

初窥Deferred

Deferred本身就是承接一组函数,在异步中执行这组函数,过去的我们是这样编写异步代码的:

       var resolve = function () {
console.log('成功');
},
//定义一个失败状态下运行的函数
reject = function () {
console.log('失败');
};
//模拟服务器请求,正在等待服务器响应ing.....
setTimeout(function (status) {
if (status === 200) resolve();
else reject();
}, 1000);

Callbacks

如果用Deferred那么上面的代码则应该是下面这个样子:

        var deferred = new Deferred();
//先将成功和失败的函数委托到Deferred
deferred.resolve(function () {
console.log('成功');
}).reject(function () {
console.log('失败');
}).resolve(function () {
console.log('我还想再追加一个成功的函数');
});
//当改变状态的时候,会自动触发成或者失败的函数
setTimeout(function (status) {
if (status === 200) deferred.resolve();
else deferred.reject();
});

Deferred

Deferred有点Callbacks的特质,不过是Callbacks的逼格提升版(异步定制版)。在Callbacks的基础上提升了对于异步函数的管理——本身的使用和Callbacks一样:承接一组函数,触发执行。

Deferred主要服役于jQuery.ajax(),使用了Deferred的ajax代码如下:

            //Deferred的应用(ajax)
$.ajax('demo.html').done(function () {
console.log('ajax成功');
}).fail(function () {
console.log('ajax失败');
}).done(function () {
console.log('ajax成功,追加一条函数处理我们自己的事情...');
});

jQuery把Deferred对象封装到ajax中,jQuery.ajax()中,jQuery维护ajax请求的发起到接收,而使用jQuery的开发者,只关注ajax的结果即可,众所周知,ajax是异步的,而我们这些成功后(失败后)要执行的函数,从代码的层面上,是线性的编写的——正是Deferred提供了这样异步编程的能力。

回调函数的定义(委托)和回调函数的执行

Deferred切割了回调函数和执行时机两个概念。就是把回调函数的定义和回调函数的执行这两个概念给分离开,同一时间专注一个概念,这样代码就能够线性的编写下去,Deferred主要应用于这种回调函数的多层嵌套,而这种情况多发生于异步(当然它也确实是为异步量身打造),所以就叫Deferred——让你异步的代码,看起来跟像同步执行一样。当然它并不局限与异步。

Deferred在Callbacks基础上做的二次封装,它封装了一组状态,每组状态对应一个Callbacks对象。我们还是说的再简单通俗一点吧:Deferred主要有三个状态作为工作标志:成功、失败、无状态。成功失败还好点,这个“无状态”是个神马意思??

Deferred本身就是根据状态来触发的,成功状态下触发成功状态的函数,失败状态下触发失败状态的函数,最后这个无状态就是:既不成功,也不失败,但是每次要触发相应的函数——用于文件上传,在文件上传的ajax中,要和服务器一直保持请求,每次请求既不代表成功也不代表失败,那么这个无状态就是最好的标志。

Deferred本质上的实现就是用数组专门用来存放对应状态的函数,然后循环执行。就是有三个数组:代表成功状态下执行的resolve函数数组和代表失败状态下执行的reject函数数组, 还有一个每次触发都会执行的progress数组。

jQuery.Deferred的Promise

jQuery.Deferred里面实现了Promise/A规范。

jQuery中,jQuery.Deferred其实本身就已经实现了Promise/A规范,并且还扩充了一套很实用的API,但是jQuery.Deferred对象中又包含着一个Promise对象,这个对象和Promise/A基本没有关联,它是切掉丁丁的jQuery.Deferred(阉割版),只有上面所说的回调函数的定义这一部分API,并没有回调函数的执行这一部分的API,这么做是因为可以在Ajax中把回调函数的执行给封闭起来(jQuery自己维护这部分),而使用jQuery的开发者则使用回调函数的定义这部分——实现一个恰到好处的观察者模式。

Deferred工作在Ajax更深层次的地方,而外层只需要根据相关结果做出对应的操作即可,即Promise对象。例如Ajax请求成功后,Deferred工作在Ajax内层(发送请求,接收请求) ,Promise对外的API接收了对应行为的函数,当内层Ajax请求成功的时候,通过Deferred标识状态为成功,那么这些承接的函数都会执行——Deferred主要应用于这样的工作场景。而因为对外开放的是Promise,它并不具备回调函数的执行这部分代码,而这部分,是被内层Deferred维护的。

这就类似你们大boss要你办一件事,并提前给了三种情形的解决方案,分别表示:这件事处理成功了之后该怎么做,处理失败了又该怎么做,处理中该怎么做。大boss给的解决方案,就是回调函数的定义,而你在这件事得到结果后针对不同的情况进行处理,就是回调函数的执行,当然,大boss也可以自己来处理这件事。并在得到结果后自己针对不同的情况进行处理,这就是Deferred和Promise对象的关系于用途。

Deferred的模型与工作原理

上面叽里呱啦说了一大堆,仍然没懂?木关系,我们直接来看模型看API就能确定这玩意儿到底是什么了,jQuery.Deferred有如下API:

API 隶属对象 描述 实现
done(function[,function...]) Deferred&Promise 添加一个或多个表示成功的函数到Deferred对象中,与Deferred.resolve()方法对应。 内部原型是Callbacks对象,该方法直接引用Callbacks.add()
fail(function[,function...]) Deferred&Promise 添加一个或多个表示失败的函数到Deferred对象中,与Deferred.reject()方法对应。 同上
progress(function[,function...]) Deferred&Promise 添加一个或多个表示无状态的函数到Deferred对象中,与Deferred.notify()方法对应,每次执行Deferred.notify()都会执行委托的回调函数,而done()、fail()方法中委托的回调函数都是一次性的。 同上
resolve([args]) Deferred 触发成功系列函数(通过Deferred.done()追加的函数),注意每次执行这些函数之后都会被销毁。 内部原型直接引用了Callbacks对象的fireWith()方法。
reject([args]) Deferred 触发失败系列函数(通过Deferred.fail()追加的函数),注意每次执行这些函数之后都会被销毁。 同上
notify([args]) Deferred 触发无状态系列函数(通过Deferred.progress()追加的函数),注意每次执行这些函数之后都会被销毁。 同上
promise([Object]) Deferred&Promise 无参的情况下返回Promise对象,有参数的情况下为参数Object扩展Promise行为。 阉割版的Deferred,内部先定义了Promise的基础API,在此基础上扩展了Deferred,就是用有参的promise()将promise的行为扩展到Deferred上的。
state() Deferred&Promise 返回当前状态的字符串:pending(尚未执行)、resolved(已成功)、rejected(已失败)、undefined(无状态,未定义) 执行相应函数的时候标识一下状态就可以了。
then(doneCallbacks[,failCallbacks[,progressCallbacks]]) Deferred&Promise 在jQuery 1.8以后被重写,委托最多三组函数到Deferred对象中,分别表示:成功、失败、无状态下执行的函数,从使用上来说,是Deferred.done()、Deferred.fail()、Deferred.progress()的简写版——然而,本质上并非如此,then方法是单独实现的——它返回一个全新的Promise对象,它连接了链式回调中的参数,让每个函数都可以与上一层、下一层函数通信,详情请见《jQuery.Deferred的then()》小节。 内部的实现较为复杂,创建了一个全新的Deferred对象(与Deferred.done())系列函数完全不同,每一次在同一个Deferred对象上链式调用then()都建立了深层的嵌套,并且通过回调函数的返回值与下一层进行通信。
always(function[,function]) Deferred&Promise 接收两个函数,分别表示成功、失败执行的函数,这才是正统的使用Deferred.done()、Deferred.fail()实现的API。 内部调用Deferred.done()、Deferred.fail()实现
other Deferred&Promise 还有一些其他的API无关痛痒啊,基本都是在上面的API基础上扩展的,so easy~~~  

从上面的API里可以看见,Promise对象就是切掉小丁丁版本的Deferred,只有回调函数的定义(done/fail/progress)API,没有回调函数的执行(resolve/reject/notify)API。下面有美图一张...

读jQuery源码 - Deferred

基础部分从代码的表现上(API的使用上)是这些:

       (function () {
//Deferred的done/resolve
var deferred = $.Deferred();
deferred.done(function (state) {
console.log(state); //write 1
});
resolve.resolve(1);
} ());
~function () {
//Deferred的fail/reject
var deferred = $.Deferred();
deferred.fail(function (state) {
console.log(state); //write 2
});
resolve.reject(2);
} ();
!function () {
//Deferred的progress/notify
/*
progress/notify对应的是“无状态”的状态
它表示一个既不表示成功,也不表示失败的状态
它每一次的触发(notify)都会执行progress里面的函数
和resolve、reject不同,通过progress委托的函数,每次notify都不会被清空
它可以反复被执行,用于会话保持
*/
var deferred = $.Deferred();
progress.progress(function (state) {
console.log(state); //write 2
});
setTimeout(function () {
//每隔1s反复执行
resolve.notify(2);
}, 1000);
} ();

Deferred

jQuery.Deferred的实现

上面说了一大堆概念啊神马的可能好多人都觉得这他瞄的什么玩意儿,直接给个痛快咱们看代码吧。

那就亮好我们的12氪钛金硬化写轮防暴+12透视*2000狗眼:

结构:

//jQuery.Deferred结构代码
jQuery.extend({
Deferred: function (func) {
var tuples = [
["resolve", "done", jQuery.Callbacks("once memory"), "resolved"],
["reject", "fail", jQuery.Callbacks("once memory"), "rejected"],
["notify", "progress", jQuery.Callbacks("memory")]
],
state = "pending",
promise = {
state: function () {
return state;
},
always: function () {
//直接调用
deferred.done(arguments).fail(arguments);
return this;
},
//then方法稍后解读
then: function ( /* fnDone, fnFail, fnProgress */) { },
promise: function (obj) {
return obj != null ? jQuery.extend(obj, promise) : promise;
}
},
deferred = {};
//过去pipe,现在的then
promise.pipe = promise.then;
jQuery.each(tuples, function (i, tuple) {
var list = tuple[2],
stateString = tuple[3];
promise[tuple[1]] = list.add;
//内部先压入三个函数
if (stateString) {
list.add(function () {
state = stateString;
//把互斥的函数和无状态函数都给禁用掉
}, tuples[i ^ 1][2].disable, tuples[2][2].lock);
}
//resolve/reject/notify
deferred[tuple[0]] = function () {
//这里的this===deferred为什么做这一层判定没有理解
//这些触发状态的方法只能是deferred拥有,既然是deferred的触发,那么为何又要阉割当前上下文呢?
deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
return this;
};
//resolveWith/rejectWith/notifyWith
deferred[tuple[0] + "With"] = list.fireWith;
});
//promise有参方法是扩展这个参数
promise.promise(deferred); //配合then使用的
if (func) {
func.call(deferred, deferred);
}
return deferred;
}
});

jQuery.Deferred

对于基础的API实现[ done/fail/progress | resolve/reject/notify],jQuery把这一部分的代码抽离出来,在后面采用循环一次性动态生成的方式实现。

var tuples = [
["resolve", "done", jQuery.Callbacks("once memory"), "resolved"],
["reject", "fail", jQuery.Callbacks("once memory"), "rejected"],
["notify", "progress", jQuery.Callbacks("memory")]
]

首先实现的promise,前面说了,promise是切掉小丁丁版本的Deferred,所以先实现promise,后面把它的API扩展到Deferred里面即可。

promise = {
state: function () {},
always: function () {},
then: function ( /* fnDone, fnFail, fnProgress */) { },
promise: function (obj) {
//有参的它为了这个参数扩展了promise行为
return obj != null ? jQuery.extend(obj, promise) : promise;
}
}

注意这个promise()方法的实现,无参的它把Promise对象的行为扩展到Deferred,后面就直接用这个方法扩展Deferred就可以让Deferred对象拥有promise的API了。

在前面准备工作完毕了之后,生成通用的部分,直接循环上面定义的通用数组,直接把Callbacks对象相应的方法引用到API上,因为我们之前Callbacks内部的实现,最终返回的都是this,这里直接引用过去之后,this就代表了Deferred/Promise对象,仍然支持链式回调。

在循环中,这里的代码很是心思慎密:

jQuery.each(tuples, function (i, tuple) {
var list = tuple[2],
stateString = tuple[3];
promise[tuple[1]] = list.add;
//内部先压入三个函数
if (stateString) {
list.add(function () {
state = stateString;
//把互斥的函数和无状态函数都给禁用掉,i^1位运算,跑下控制台就知道了
}, tuples[i ^ 1][2].disable, tuples[2][2].lock);
}
//resolve/reject/notify
deferred[tuple[0]] = function () {
//这里的this===deferred为什么做这一层判定没有理解
//这些触发状态的方法只能是deferred拥有,既然是deferred的触发,那么为何又要阉割当前上下文呢?
deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
return this;
};
//resolveWith/rejectWith/notifyWith
deferred[tuple[0] + "With"] = list.fireWith;
});

stateString取值范围(上面数组的定义中)有三个:"resolved"、"rejected"、undefined。

所以进入这个判定之后,变量i的值只可能是0||1。

然后给Callbacks中压入三个响应的回调函数,分别执行了修改状态字符串、将互斥的函数设置为不可用、锁定无状态的函数,后两个直接引用了Callbacks的方法。这里:通过位运算符得到互斥的索引,然后根据索引访问上面数组里对应的Callbacks,直接禁用和锁定。

也就是说,Deferred默认为每个状态压入了三个函数,当我们使用done/fail/progress的时候,是在这三个函数之后执行的,当首次执行触发状态函数(resolve/reject/notify),先执行了这三个函数,再来美图一张,演示了Deferred整个内部模型:

读jQuery源码 - Deferred

后面的代码,木有了!!!!你木有看错,是真的木有了!!!有这么一点点啊!真的就这么一点点代码!!!有木有感觉so easy?随手就写了一个Deferred有木有啊?!

小伙鸡,还有一个大块头呢,不要忽略这个API——Deferred.then()!

jQuery.Deferred的then()

这个then()啊,很是巧妙,读起来就简直就是各种痛经啊。我们先再来详细撸一发then()的定义。

then的定义:

Promise和Deferred共同拥有API——then():上面的源码里可以看见,Deferred里面本质上是三个Callbacks在工作,,分别存放着不同状态下都要执行的函数列表,看过别人的解释:如果我们添加一个成功状态下要执行的函数,那么大家可能想着调用Deferred.done()。而then()呢,提供了一个便捷的API,then()接收三个参数,分别表示:成功状态下执行的函数,失败状态下执行的函数,每次触发状态下执行的函数——其实意思上就是把done/fail/progress合并到了一个API。

嗯,这是在jQuery.1.8以前then()的实现,在jQuery.1.8以前,then()只是一个普通的实现,1.7.2中它的实现:

 then: function (doneCallbacks, failCallbacks, progressCallbacks) {
deferred.done(doneCallbacks).fail(failCallbacks).progress(progressCallbacks);
return this;
}

可以看见只是就是直接调用了Call自己的API啊,真真正正的提供了便捷的API入口。我们还是撸一下then的前世吧,在jQuery.1.8以前,有一个APIDeferred.pipe():这API的作用是:提供一个类似always()的API,也就是三个参数,分别表示done、fail、progress状态的函数,也就是把这个三个API合并到一起了,同时,这些函数都可以沟通。

jQuery.1.8以后,Deferred.pipe()过时,取代它的API就是Deferred.then()。

什么叫做这些函数可以沟通?看如下代码(Deferred.then):

//普通的应用
!(function () {
var deferred = $.Deferred();
deferred.done(function (value) {
return value * 10;
})
.done(function (value) {
console.log(value);
});
deferred.resolve(1); //result ---- 1
})(); //then的应用
!(function () {
var deferred = $.Deferred();
deferred.then(function (value) {
return value * 10;
}).then(function (value) {
console.log(value);
});
deferred.resolve(1); //result ---- 10
})();

Deferred.then

通过then()添加的函数,同一状态下,上一个函数的返回值可以传递到下一层,这就是then/pipe的实现。

Deferred.then:

then: function ( /* fnDone, fnFail, fnProgress */) {
//保存参数
var fns = arguments;
//注意这里的Deferred,Deferred参数如果是一个函数,那么会直接执行这个函数,参数就是闭包里的deferred对象!!
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
//注意这里已经把then()里面的函数封装到了上一层deferred对象中
deferred[tuple[1]](function () {
var returned = fn && fn.apply(this, arguments);
if (returned && jQuery.isFunction(returned.promise)) {
//这一层判定的判定扩展了函数返回的Promise/Deferred对象,这里应该是给jQuery.when()方法使用的
returned.promise()
.done(newDefer.resolve)
.fail(newDefer.reject)
.progress(newDefer.notify);
//其实这里的扩展,应该只是纯粹的对具有promise/A的扩展,只是留了这个功能,什么时候执行,并不是jQuery.Deferred关心的事情
} else {
//这是then方法的本质,使用then()返回的promise对象依赖于newDefer对象
//then方法中,这里把上一层的返回值传递到下一层
//而现在的环境只能被最顶层的Deferred触发
//在触发顶层的Deferred中,触发then()中的Deferred
//这里的判定,为什么要做这一层对象的封装呢?
newDefer[tuple[0] + "With"](this === promise ? newDefer.promise() : this, fn ? [returned] : arguments);
}
});
});
//这里可以放心释放fns,在上面的each中,已经单独创建了对应了变量
fns = null;
}).promise();
}

Deferred.then

实现上比较饶,做了这些事情:

  • 1、创建了一个新的Deferred对象,Deferred对象构造函数里,如果传入一个函数作为参数,那么这个函数就会立即执行,这个函数的参数和上下文,就是新创建的Deferred对象。
  • 2、因为then的API承接done/fail/progress这些函数,所以循环上面定义的那个公共部分的数组,一次循环三个函数一并处理了。
  • 3、在每次循环中,创建一个匿名的函数,添加到上一层的Deferred对象中,通过done/fail/progress添加,所以这个函数,会在上一层Deferred对象标志状态的时候(resolve/reject/notify)被执行,这一步其实是在封装通过then()添加进来的函数。
  • 4、在匿名函数中,执行通过then()添加进来的对应状态的函数,并获取到返回值。
  • 5、做了一次返回值的判定,如果这个返回值拥有promise/A的行为,则把当前Deferred对象里面所有的函数扩展到这个返回值对象中,注意是当前Deferred,而不是闭包外的Deferred,then中当前Deferred和then之外的Deferred是两个对象。
  • 6、如果这个返回值不具有promise/A的行为,则直接执行当前Deferred对象相应标识状态的函数(resolve/reject/notify)

这里的代码如下几点需要注意:

作用域:newDefer是then中新创建的Deferred对象,then最终返回的是这个对象的Promise,而在这个newDefer中通过done/fail/progress压入的函数,都是压在上一层Deferred中,也就是变量deferred。

执行链:then中,一开始有参数newDefer的大匿名函数,是在新的Deferred对象里执行的,而在这里面,又通过上一层的变量deferred对应的done/fail/progress添加的匿名函数,在添加的匿名函数里,又调用了newDefer对应的resolve/reject/notify——即上一层驱动了下一层的执行,读透它需要多一点思考。

手贱又画了张图,美图一张:

读jQuery源码 - Deferred

思考:

代码的阅读:

我读代码的时候先读的Deferred的基础部分,最后单独读then()的,基础部分通俗易懂,公共的数组和现有API的利用非常巧妙。代码上,个人觉得我读的版本jQuery.1.11.1代码整理的非常精致,但是阅读起来略感晦涩,读完了之后个人也读了jQuery.1.7.2(以前一直用1.7.2的),觉得后者的代码整理上不如前者,但是相比前者阅读上更加的通俗易懂和简单明了。

then:

then是实现很是精髓,尤其要理解每一段代码会产生的作用,jQuery在then里面有这么一段代码:

if (returned && jQuery.isFunction(returned.promise)) {
returned.promise()
.done(newDefer.resolve)
.fail(newDefer.reject)
.progress(newDefer.notify);
}

这里的代码琢磨了好久为什么,会发生什么,各种代码模拟尝试,思考了一下,如果我们委托的函数返回一个具有promise行为的对象,那么这里就提供了停下后续函数的执行这么一个实现,并且,会把当前Deferred对象里所有未执行的函数(done/fail/progress)都传递给这个具有promise行为的对象。

then的职责:

为了让函数的可以沟通,实现了then,而then一直用jQuery.Deferred创建新的实例,这么做主要的作用是每个不同的Deferred.then()他们的沟通是被隔离的,如下代码:

!function () {
//then中上下文被隔离
var deferred = $.Deferred();
deferred.then(function (value) {
return value * 10;
});
//then创建了新的Deferred
deferred.then(function (value) {
console.log(value);//还是1,他们的上下文被隔离了
});
deferred.resolve(1);
} ();
!function () {
//then中使用不被隔离的上下文
var deferred = $.Deferred();
deferred.then(function (value) {
return value * 10;
}).then(function (value) {
//在上一个then的返回值上调用,所以上下文没有被隔离
console.log(value);//
});
deferred.resolve(1);
} ();

Deferred.then

如上代码所示,每个不同的then都被隔离了:Deferred.then和done/fail/progress之间不同的地方就是让每个相同状态的函数都可以沟通(通过返回值),而每个Deferred.then(每次独立调用Deferred.then)之间的不同则会让作用域被隔离。

我思考过then的重构:尝试着把每个函数都可以沟通这个概念直接应用在done/fail/progress上面,这样就不用在then中一直创建新的Deferred对象,但是这么做就让done/fail/progress承载了太多,每次对函数执行后的返回值做判定,这根本就是不靠谱的做法,并且失去了上面所说的隔离沟通,看起来好像Deferred更加的平滑了,其实这样会让Deferred应用条件变得更加苛刻。反观jQuery.Deferred,每一个API做的不多,但是足够细腻与精准,只能说我还是图样图森破啊...

每个函数都有自己的职责,不要让它承载的太多,太多的职责决定了这个函数会越发的不可控。我想,这或许也是jQuery.then单独实现的一个理由吧。

类,往往因为承载的太多而变得臃肿不堪。

最后,我手抄了一份jQuery.Deferred的代码,可以单独运行,并加入了注释。

如果你觉得这篇文章不错的话,点一下右下角的推荐吧。举手之劳,却鼓舞人心,何乐而不为呢?

源码

作者:linkFly
声明:嘿!你都拷走上面那么一大段了,我觉得你应该也不介意顺便拷走这一小段,希望你能够在每一次的引用中都保留这一段声明,尊重作者的辛勤劳动成果,本文与博客园共享。