jQuery 2.0.3 源码分析 Deferred概念
JavaScript编程几乎总是伴随着异步操作,传统的异步操作会在操作完成之后,使用回调函数传回结果,而回调函数中则包含了后续的工作。这也是造成异步编程困难的主要原因:我们一直习惯于“线性”地编写代码逻辑,但是大量异步操作所带来的回调函数,会把我们的算法分解地支离破碎。此时我们不能用if来实现逻辑分支,也不能用while/for/do来实现循环,更不用提异步操作之间的组合、错误处理以及取消操作了。因此也诞生了如jQuery Deferred这样的辅助类库
举一个例子会有助于理解
我做的是phonegap项目,所以涉及到的异步处理就别特多:
1. 常见的setTimeout
2. 与底层代码通信,调用插件cordova.exec
3. postmessage与iframe通信
4. CSS3 动画
5. Ajax
6. HTML5的本地数据
等等…
我就拿HTML5 Web Database为例:
数据库
查询语句
//数据库链接对象 config.db = window.openDatabase(config.dbName, "1.0", "Xxtebook Database", config.dbSize); config.db.transaction(function(tx) { tx.executeSql('SELECT * FROM Novel'
, [], function(tx, results) { result = results.rows; console.log('result',result) // }); }, function(){}, function(){});
就这段代码其实本身是没问题的,但是这里至少要涉及到异步方面的考虑:
javaScript要求在与服务器进行交互时要用异步通信,如同AJAX一样,因为是异步模型,所以在调用transaction游览器提供的本地数据接口时候类似AJAX(这里我是假设),浏览器自己有内部的XHR方法异步处理,但是此时的JS代码还是会同步往下执行,其实就是无阻塞的代码
问题:因为无无阻塞,代码在发送AJAX这个请求后会继续执行,那么后续的操作如果依赖这个数据就会出错了,所以这里就需要等待AJAX返回,才能执行后续操作
我们换个更简单 伪代码
alert(1) setTimeout(function(){ alert(2) },0) alert(3) //alert(1) //alert(3) //alert(2)
代码都能看懂,因为异步而导致流程不正确
或者说我们的应用在某个程度上依赖第三方api的数据,那么就会面临一个共同的问题:
我们无法获悉一个API响应的延迟时间,应用程序的其他部分可能会被阻塞,直到它返回 结果。Deferreds 对这个问题提供了一个更好的解决方案,它是非阻塞的,并且与代码完全解耦 。
当然异步操作也可以提供一个类似成功回调,失败回调的通知接口
JS是单线程语言,就简单性而言,把每一件事情(包括GUI事件和渲染)都放在一个线程里来处理是一个很好的程序模型,因为这样就无需再考虑线程同步这些复杂问题。
另一方面,他也暴露了应用开发中的一个严重问题,单线程环境看起来对用户请求响应迅速,但是当线程忙于处理其它事情时,就不能对用户的鼠标点击和键盘操作做出响应。
What's The Point Of Promises?
http://www.kendoui.com/blogs/teamblog/posts/13-03-28/what-is-the-point-of-promises.aspx
http://wiki.commonjs.org/wiki/Promises/A
大多情况下,promise作为一个模型,提供了一个在软件工程中描述延时(或将来)概念的解决方案。
它背后的思想我们已经介绍过:不是执行一个方法然后阻塞应用程序等待结果返回,而是返回一个promise对象来满足未来值。
这样看来,Promise/A只是一种规范,Deferred可以看作这种规范的具体实现
画了一个很简短的图,大家略过一下,后面有很详细的流程图
我们先看看规范如何定义的:
Promise/A提议’定义了一个’then‘方法来注册回调,当处理函数返回结果时回调会执行。它返回一个promise的伪代码看起来是这样的:
promise = function create( //数据库链接对象 config.db = window.openDatabase(config.dbName, "1.0", "Xxtebook Database", config.dbSize); config.db.transaction(function(tx) { tx.executeSql('SELECT * FROM Novel', [], function(tx, results) { result = results.rows; console.log('result',result) // }); }, function(){}, function(){}); ); promise.then(function( futureValue ) { }); promise.then(function( futureValue ) { });
promise回调会在处于以下两种不同的状态下执行:
- resolved:在这种情况下,数据是可用
- rejected:在这种情况下,出现了错误,没有可用的值
'then'方法接受两个参数:一个用于promise得到了解决(resolved),另一个用于promise拒绝(rejected)。让我们回到伪代码
promise.then( function( futureValue ) { //成功 } , function() { //失败 } );
在某些情况下,我们需要获得多个返回结果后,再继续执行应用程序(例如,在用户可以选择他们感兴趣的选项前,显示一组动态的选项)。这种情况下,'when'方法可以用来解决所有的promise都满足后才能继续执行的场景。
when( promise1, promise2, ... ).then(function( futureValue1, futureValue2, ... ) { //当所有的异步对象都准备完毕后,才执行 });
我们在模拟一个场景
如果同时执行多个动画的时候,我们就需要对每一个动画执行完毕后的处理下一个动作,意味着我们就要监听所有动画的执行完毕后的回调
使用promise和‘when’方式却可以很直截了当的表示: 一旦动画执行完成,就可以执行下一步任务。最终的结果是我们可以可以简单的用一个回调来解决多个动画执行结果的等待问题
when( function(){ //执行动画1 return promise }, function(){ //执行动画2 return promise 2 } ).then(function(){ //当2个动画完成后,我们在自行后续的代码 });
基本上可以用非阻塞的逻辑方式编写代码并异步执行。 而不是直接将回调传递给函数,这可能会导致紧耦合的接口,通过promise模式可以很容易区分同步和异步的概念。
根据这个规范,我们看看JQ是如何实现的?
Deferred Object :http://api.jquery.com/category/deferred-object/
由于1.7版本后回调函数Callbacks从Deferred中抽离出去了,所以在此之前还需要了解下 回调函数-callback
本文主要是涉及源码的实现
我们先对API有个整体的认识
jQuery.Deferred() |
一个构造函数,返回一个链式实用对象方法来注册多个回调,回调队列, 调用回调队列,并转达任何同步或异步函数的成功或失败状态。 |
deferred.always() |
当Deferred(延迟)对象解决或拒绝时,调用添加处理程序 |
deferred.done() |
当Deferred(延迟)对象解决时,调用添加处理程序 |
deferred.fail() |
当Deferred(延迟)对象拒绝时,调用添加处理程序。 |
deferred.isRejected() |
确定一个Deferred(延迟)对象是否已被拒绝。 |
deferred.isResolved() |
确定一个Deferred(延迟)对象是否已被解决。 |
deferred.notify() |
根据给定的 args参数 调用Deferred(延迟)对象上进行中的回调 (progressCallbacks) |
deferred.notifyWith() |
根据给定的上下文(context)和args递延调用Deferred(延迟)对象上进行中的回调(progressCallbacks ) |
deferred.pipe() |
实用的方法来过滤 and/or 链Deferreds。 |
deferred.progress() |
当Deferred(延迟)对象生成进度通知时,调用添加处理程 |
deferred.promise() |
返回Deferred(延迟)的Promise(承诺)对象。 |
deferred.reject() |
拒绝Deferred(延迟)对象,并根据给定的args参数调用任何失败回调函数(failCallbacks) |
deferred.rejectWith() |
拒绝Deferred(延迟)对象,并根据给定的 context和args参数调用任何失败回调函数(failCallbacks)。 |
deferred.resolve() |
解决Deferred(延迟)对象,并根据给定的args参数调用任何完成回调函数(doneCallbacks) |
deferred.resolveWith() |
解决Deferred(延迟)对象,并根据给定的 context和args参数调用任何完成回调函数(doneCallbacks) |
deferred.state() |
确定一个Deferred(延迟)对象的当前状态 |
deferred.then() |
当Deferred(延迟)对象解决,拒绝或仍在进行中时,调用添加处理程序。 |
jQuery.when() |
提供一种方法来执行一个或多个对象的回调函数, Deferred(延迟)对象通常表示异步事件。 |
.promise() |
返回一个 Promise 对象用来观察当某种类型的所有行动绑定到集合,排队与否还是已经完成 |
构建一个异步对象:
我们传统的写法
function aaa(callback){ setTimeout(function(){ callback( 5 ); },1000); } function done(value){ alert(value) } aaa(function(value){ done(value); })
换成JQuery的写法
var defer = $.Deferred(); //构建异步对象 setTimeout(function(){ defer.resolve( 5 ); },1000); var filtered = defer.then(function( value ) { return value * 2; }); filtered.done(function( value ) { console.log('打印出值',value) });
通过对比我们还是能感觉到很大的不同:
传统的代码逻辑,通过嵌套回调函数,等待异步发送成功的通知后,在执行下一步操作,如果有大量的嵌套回调,耦合度,维护性,扩展性?
那么Deferred对象存在的意义在哪里?
从我角度来说,对于开发者
1:代码可读性,扁平化结构
2:让支离破碎的代码结构,继续保存线性的代码逻辑,也就是异步代码转为同步
3: 从抽线的角度,提供了一个抽象的非阻塞的解决方案(如 Ajax 请求的响应),它创建一个 “promise” 对象,其目的是在未来某个时间点返回一个响应。
deferreds 可以理解为表示需要长时间才能完成的耗时操作的一种方式,相比于阻塞式函数它们是异步的,而不是阻塞应用程序等待其完成然后返回结果。 deferred对 象会立即返回,然后你可以把回调函数绑定到deferred对象上,它们会在异步处理完成后被调用。
所以,总的来讲Deferred通过一组 API 来规范化异步操作,这样也能够让异步操作的流程控制更加容易
下章就是具体的源码分析了,会有附带流程图及源码的分析
类似的异步库可以参考
http://www.cnblogs.com/aaronjs/p/3169328.html
http://www.cnblogs.com/aaronjs/p/3168588.html
基本都大同小异