jQuery2.x源码解析(回调篇)
通过艾伦的博客,我们能看出,jQuery的promise和其他回调都是通过jQuery.Callbacks实现的。所以我们一起简单看看jQuery.Deferred和jQuery.Callbacks。来看看关于他们的一些提问。
提问:jQuery.Callbacks的配置为什么是用字符串参数?
jQuery.Callbacks有四种配置,分别是once、memory、unique、stopOnFalse。而jQuery.Callbacks的配置形式却和以往我们熟悉的不同,不是使用json,而是使用字符串的形式配置的,这是为什么呢?
答:jQuery.Callbacks的配置形式,确实很怪异,jQuery.Callbacks使用了一个createOptions函数将字符串转为了json。
var rnotwhite = ( /\S+/g );
function createOptions( options ) {
var object = {};
jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
object[ flag ] = true;
});
return object;
}
如"once memory unique"字符串,最终会转为{"once":true,"memory":true,"unique":true}。但是笔者想问为什么不直接使用json呢?
像这种使用字符串配置的形式,JavaScript中也是有的,js中的正则表达式就是这种配置形式。
如:
var patt = new RegExp("e","gim");
这里的“gim”就是配置项。但是这种配置方法很难让人理解,这样不符合jQuery追求的理念,所以笔者认为jQuery.Callbacks使用字符串配置代替数组配置是jQuery里面的一个败笔。
提问:jQuery.Deferred封装的函数一定是异步的吗?
答:这是笔者以前一直疑惑的一个问题。我们先跳出jQuery,先使用标准的JavaScript,看看他的异步函数一定是异步的吗?
测试代码如下:
setTimeout(function(){
console.log(0);
},0);
setTimeout(function(){
console.log(1);
},0);
console.log(2);
非常简单的一行代码,我们也很清楚输出结果是“201”。JavaScript是单线程执行的,所以异步函数的回调会在后边执行。同时异步的回调函数会被放入队列中,所以会按照进入队列的顺序执行下来。注意setTimeout的时间参数一定要给0,因为setTimeout的回调时间默认值不一定是0。
我们把中间的输出1的函数的setTimeout去除,让其不再异步。
setTimeout(function(){
console.log(0);
},0);
console.log(1);
console.log(2);
结果变为了“120”。这个结果不需要作说明,仅是为了做对比而做的实验。
现在换位使用异步函数的语法,将输出1的部分用es7的异步函数包裹:
async function test(){
await Date.now(); //注意test执行的时候如果不给await修饰,test虽然是一个异步函数,但是在异步部分是没有可执行代码的。console.log(1)会在同步部分执行
console.log(1);
}
setTimeout(function(){
console.log(0);
},0);
test();
console.log(2);
结果是“210”。我们知道上面的Date.now并不是一个异步函数,但是console.log(1)还是在console.log(2)执行,说明await后面的代码是在异步回调中运行的。
我们再将异步函数转为promise。
setTimeout(function(){
console.log(0);
},0);
new Promise(function(resolve){
resolve();
}).then(()=>{
console.log(1)
})
console.log(2);
上述代码应该和异步函数结果相同,结果也确实相同,结果是“210”。
从这些例子我们可以看出,使用promise封装的函数(异步函数),其回调部肯定是异步回调。
现在我们用jQuery.Deferred封装的promise再将上面的实现进行一次。
setTimeout(function(){
console.log(0);
},0);
$.Deferred().resolve().then(function(){
console.log(1);
});
console.log(2);
如上的结果是“120”。这说明jQuery的promise和标准的promise的表现形式是不一样的。jQuery.Deferred的回调,如果resolve不是在异步回到中执行的,那么then里面的回调也不会在异步回调中执行,这与标准的promise是不同的。
我们也可以看看jQuery.Callbacks源码,里面是不含有setTimeout,或者其他可以返回异步回调的。不过这是jQuery2.0的结果,jQuery3.0的结果是“201”,有兴趣的话大家可以自己尝试。
提问:jQuery.Callbacks的回调中的this指向什么?
答:简单的分析一下源码结构:
jQuery.Callbacks = function( options ) {
var list = [];
var fire = function() {
list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) ;
}
return {
add: function() {
( function add( args ) {
jQuery.each( args, function( _, arg ) {
list.push( arg );
} );
} )( arguments );
return this;
},
fireWith: function( context, args ) {
args = [ context, args.slice ? args.slice() : args ];
queue.push( args );
if ( !firing ) {
fire();
}
return this;
},
fire: function() {
self.fireWith( this, arguments );
return this;
},
};
};
从这段代码可以清楚看到,通过add我们将函数保存在内部私有变量list里面,然后使用apply调用。对外暴露的函数有fireWith和fire。fireWith的context参数是最终传递给了apply,所以是我们回调中的this就是这个context。而fire函数里面调用了fireWith,传递的是自身this,所以回调的函数中的this是Callbacks对象。
提问:jQuery.Deferred的回调中的this指向什么?
答:标准的promise中的this,是指向全局作用域的,例如window。
var d = new Promise((r)=>{r()})
d.then(function(){console.log(this)}) //注意,此处不可以用()=>{console.log(this)}
输出的是window,证明列我们之前的说法。
我们再来看看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 = {
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
deferred[ tuple[ 1 ] ]( function() {
var returned = fn && fn.apply( this, arguments );
if ( returned && jQuery.isFunction( returned.promise ) ) {
returned.promise()
.progress( newDefer.notify )
.done( newDefer.resolve )
.fail( newDefer.reject );
} else {
newDefer[ tuple[ 0 ] + "With" ](
this === promise ? newDefer.promise() : this,
fn ? [ returned ] : arguments
);
}
} );
} );
fns = null;
} ).promise();
},
promise: function( obj ) {
return obj != null ? jQuery.extend( obj, promise ) : promise;
}
},
deferred = {};
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 );
}
deferred[ tuple[ 0 ] ] = function() {
deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments );
return this;
};
deferred[ tuple[ 0 ] + "With" ] = list.fireWith;
} );
promise.promise( deferred );
if ( func ) {
func.call( deferred, deferred );
}
return deferred;
}
} );
代码中构建了两个对象——deferred和promise。deferred相当于对异步过程的封装,而promise是对promise模型的封装。代码的最后将promise织入到deferred中。
resolve、reject、notify是promise的三种设置状态的函数,实际上这三个函数的执行策略相似,所以jQuery采用策略模式,用一个二维数组tuples将这3种策略封装起来。
代码下半部分对tuples的遍历,我们可以看到,jQuery实现了promise的done、fail、progress这三个函数,这三个函数实现方式都是对jQuery.Callbacks.add的封装。对deferred扩展了六个函数——resolve、reject、notify和resolveWith、rejectWith、notifyWith,前三个函数是对后三个函数的封装,后三个函数都是对jQuery.Callbacks.fireWith封装的。前三个函数调用后三个函数的时候,context参数都是传递的promise(除非回调函数被用bind、reject、resolve改变了上下文)。所以当deferred执行resolve、reject、notify的时候,回调函数的this是promise对象;当this使用resolveWith、rejectWith、notifyWith,或者用bind、call、apply改变了resolve、reject、notify的执行上下文的时候,回调的this是指向给定的对象。
再看上边then方法的定义,同样是遍历tuples。tuples数组定义的done、fail、progress这三个函数名称,与then的三个参数一致(done、catch、notify),这三个参数都是回调函数。回调他们的方式是deferred的done、fail、progress三个函数,我们知道这三个函数是使用promise织入进来的,真正的方法是promise的resolve、reject、notify这三个函数,因此then中的回调的this也是指向的是promise或者之前被指定的上下文。
this指向promise其实并没有太多意义,而通过xxxWith函数或者用bind、call、apply改变了resolve、reject、notify的上下文的方式调用,才是jQuery的亮点。jQuery提供这样的api的目的是为了我们可以指定promise的this,这样貌似更灵活,更方便我们操作回调。
但是,回调中this的不同,是jQuery.Deferred和标准promise一个很大的区别,这是不标准的用法,这一点一定要切记。jQuery提供的api虽然很方便,但是这样改变列promise模型,是不推荐的用法,尤其是promise如今已经收纳到es6的语法中,es7的异步语法也是基于promise的,在不支持promise的浏览器上创建出标准的promise才是jQuery更该做的,因为只有这样才能实现promise语法的对接。
提问:为什么将promise织入到deferred中?
答:按照jQuery的思路,deferred相当于对异步过程的封装,是promise的创建者与指挥者,但是根据jQuery的思路,将二者统一能更好的简化异步对象的模型,如:
var d = $.Deferred();
d.resolve();
d.promise().then(()=>{
...
}) //等效于
var d = $.Deferred();
d.resolve()
.then(()=>{
...
})
下边的写法是不是更加简单紧凑呢?同时也符合jQuery的链式操作。简单来说,少了一个对象的概念,大家当然更容易理解。
事实上,promise还可以织入到其他对象中,如:
var d = $.Deferred();
d.resolve();
d.promise(myObj);
myObj.then(()=>{
...
})
通过这种方式,jQuery可以很灵活的把promise的操作嵌入到任何对象中,非常方便。
提问:jQuery.Deferred.promise()有没有实现promises/A+吗?
答:是的,这个无需看源码,仅看api也很清楚,jQuery.Deferred.promise()没有实现promises/A+。例如jQuery增加了done、fail、progress等函数,这三个函数都不是promises/A+模型标准的函数名,但是却近似实现了promises/A+的概念模型。同时,promises/A+里面有catch函数,但是jQuery却没有实现,主要原因是catch是早期的ie浏览器中的关键字,因此使用了fail代替。done、progress是jQuery根据自身需要进行的扩展。
jQuery.Deferred.promise和promises/A+还有一个重要区别,就是对异常的处理。实例代码如下:
d = new Promise((resolve)=>{resolve()})
d.then(()=>{throw "error"})
.catch((e)=>{alert(e)})
标准的promise弹出error字样的alert框。
再换位jQuery.Deferred.promise测试:
$.Deferred().resolve().then(()=>{throw "error"}).fail((e)=>{alert(e)})
结果直接报错,并没有弹出alret框。
源码中jQuery没有对回调的调用做异常处理,所以无法把回调的异常自动返回rejected的promise。这一点不同使得jQuery.Deferred的异常处理非常不灵活,需要手动进行。修改为:
$.Deferred()
.resolve()
.then(()=>{
try{
throw "error"
}catch(e){
return $.Deferred().reject(e);
}
}).fail((e)=>{
alert(e)
})
这样才能弹出error字样的alert框。
提问:jQuery.Deferred.promise()能和浏览器的promise相互调用吗?
答:当然不能直接调用,需要做转换,转换代码这里就不演示了。不能直接调用的原因是jQuery.Deferred.promise没有实现promises/A+。但是这只是一部分原因,最大的原因还是历史问题,jQuery2.0的时候,浏览器的promise模型还没有建立,所以根本不可能去考虑与浏览器的promise相互调用的问题。
提问:如果想要jQuery.Deferred.promise()能和浏览器的promise直接调用,jQuery.Deferred.promise需要做哪些改善?
答:首先要对promises/A+做到完整实现,去掉jQuery个性化的东西,如xxxWith调用、Deferred代替promise、done等不规范的函数命名。因此,在jQuery3中jQuery.Deferred做了不可升级式的调整,新的jQuery.Deferred.promise提供了一套新的api,完全实现了promises/A+规范,并且可以调用浏览器的promise相互调用。
jQuery2.x源码解析(回调篇)的更多相关文章
- jQuery2.x源码解析(缓存篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 缓存是jQuery中的又一核心设计,jQuery ...
- jQuery2.x源码解析(构建篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 笔者阅读了园友艾伦 Aaron的系列博客< ...
- jQuery2.x源码解析(设计篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...
- jQuery2.x源码解析(DOM操作篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) jQuery这个类库最为核心重要的功能就是DOM ...
- Shiro源码解析-Session篇
上一篇Shiro源码解析-登录篇中提到了在登录验证成功后有对session的处理,但未详细分析,本文对此部分源码详细分析下. 1. 分析切入点:DefaultSecurityManger的login方 ...
- myBatis源码解析-类型转换篇(5)
前言 开始分析Type包前,说明下使用场景.数据构建语句使用PreparedStatement,需要输入的是jdbc类型,但我们一般写的是java类型.同理,数据库结果集返回的是jdbc类型,而我们需 ...
- Spring源码解析 | 第二篇:Spring IOC容器之XmlBeanFactory启动流程分析和源码解析
一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...
- myBatis源码解析-数据源篇(3)
前言:我们使用mybatis时,关于数据源的配置多使用如c3p0,druid等第三方的数据源.其实mybatis内置了数据源的实现,提供了连接数据库,池的功能.在分析了缓存和日志包的源码后,接下来分析 ...
- myBatis源码解析-反射篇(4)
前沿 前文分析了mybatis的日志包,缓存包,数据源包.源码实在有点难顶,在分析反射包时,花费了较多时间.废话不多说,开始源码之路. 反射包feflection在mybatis路径如下: 源码解析 ...
随机推荐
- 【原】nodejs全局安装和本地安装的区别
来微信支付有2年多了,从2年前的互联网模式转变为O2O模式,主要的场景是跟线下的商户去打交道,不像以往的互联网模式,有产品经理提需求,我们帮忙去解决问题. 转型后是这样的,团队成员更多需要去寻找业务的 ...
- Python应用03 使用PyQT制作视频播放器
作者:Vamei 出处:http://www.cnblogs.com/vamei 严禁任何形式转载. 最近研究了Python的两个GUI包,Tkinter和PyQT.这两个GUI包的底层分别是Tcl/ ...
- Redis百亿级Key存储方案(转)
1 需求背景 该应用场景为DMP缓存存储需求,DMP需要管理非常多的第三方id数据,其中包括各媒体cookie与自身cookie(以下统称supperid)的mapping关系,还包括了supperi ...
- 【详细教程】论android studio中如何申请百度地图新版Key中SHA1值
一.写在前面 现在越来越多的API接口要求都要求提供我们的项目SHA1值,开发版目前还要求不高,但是发布版是必定要求的.而目前定位在各大APP中也较为常见,当下主流的百度地图和高德地图都在申请的时候会 ...
- [原] KVM 环境下MySQL性能对比
KVM 环境下MySQL性能对比 标签(空格分隔): Cloud2.0 [TOC] 测试目的 对比MySQL在物理机和KVM环境下性能情况 压测标准 压测遵循单一变量原则,所有的对比都是只改变一个变量 ...
- 微服务与Docker介绍
什么是微服务 微服务应用的一个最大的优点是,它们往往比传统的应用程序更有效地利用计算资源.这是因为它们通过扩展组件来处理功能瓶颈问题.这样一来,开发人员只需要为额外的组件部署计算资源,而不需要部署一个 ...
- ABP项目中使用Swagger生成动态WebAPI
本文是根据角落的白板报的<使用ABP实现SwaggerUI,生成动态webapi>一文的学习总结,感谢原文作者角落的白板报. 1 安装Swashbuckle.core 1.1 选择WebA ...
- BPM配置故事之案例9-根据表单数据调整审批线路2
老李:好久不见啊,小明. 小明:-- 老李:不少部门有物资着急使用,现在的审批流程太慢了,申请时增加一个是否加急的选项吧.如果选加急,金额1000以下的直接到我这里,我审批完就通过,超过1000的直接 ...
- SQLServer2005创建定时作业任务
SQLServer定时作业任务:即数据库自动按照定时执行的作业任务,具有周期性不需要人工干预的特点 创建步骤:(使用最高权限的账户登录--sa) 一.启动SQL Server代理(SQL Server ...
- MySQL,MariaDB:Undo | Redo [转]
本文是介绍MySQL数据库InnoDB存储引擎重做日志漫游 00 – Undo LogUndo Log 是为了实现事务的原子性,在MySQL数据库InnoDB存储引擎中,还用Undo Log来实现多版 ...