异步JS:$.Deferred的使用
原文链接:http://www.html5rocks.com/en/tutorials/async/deferred/
当我们构建一个平稳的,响应式的HTML5应用时,其中一个非常重要的方面是在不同部分的应用中的同步,例如数据获取,程序处理, 动画和用户界面元素。
在桌面和原生环境之间,一个主要的区别就是浏览器不给访问线程模型,但会为用户界面(例如DOM)提供一个单线程的访问。这意味着所有的应用程序逻辑访问和修改用户界面元素总是在同一线程中,因此要保证程序的工作单位尽可能的短小和高效,以及尽量多的使用更有优势的浏览器提供的异步能力。
浏览器异步APIs
很幸运,浏览器提供了一些异步API,例如很常用的XHR(XMLHttpRequest或AJAX)API,还有IndexedDB, SQLite, HTML5 Web workers和HTML5 GeoLocation (地理定位)API,甚至一些DOM相关的行为都有异步,例如通过transitionEnd的事件的CSS3动画。
浏览器暴露异步编程给程序逻辑的方式是通过事件或者回调。在基于事件的异步API中,开发者给一个已存在的对象(例如HTML元素或者其他DOM对象)注册一个事件处理程序,然后执行它。浏览器通常会在不同的线程上表现行为,适当的时候会在主线程中触发事件。
举个例子,写段有段XHR API,一个基于事件的异步API,将会像这样:
// Create the XHR object to do GET to /data resource
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
alert("We got data: " + xhr.response);
}
},false)
// perform the work
xhr.send();
另一个基于事件的异步API CSS3 transitionEnd事件:
// get the html element with id 'flyingCar'
var flyingCarElem = document.getElementById("flyingCar");
// register an event handler
// ('transitionEnd' for FireFox, 'webkitTransitionEnd' for webkit)
flyingCarElem.addEventListener("transitionEnd",function(){
// will be called when the transition has finished.
alert("The car arrived");
});
// add the CSS3 class that will trigger the animation
// Note: some browers delegate some transitions to the GPU , but
// developer does not and should not have to care about it.
flyingCarElemen.classList.add('makeItFly')
其他的浏览器API,例如SQLite和HTML5地理定位都是基于回调的。
这是一段HTML5地理定位的例子:
// call and pass the function to callback when done.
navigator.geolocation.getCurrentPosition(function(position){
alert('Lat: ' + position.coords.latitude + ' ' +
'Lon: ' + position.coords.longitude);
});
在这个例子中,我们执行了一个方法和将一个函数作为参数传入,然后将会得到请求的结果。这使得浏览器能够同步或异步方式实现这个功能,给一个单一的API给开发者,而不管实施细节
开始使用异步
除了浏览器内置的异步API,良好构建的应用程序也应该将它们的低级(基础)的API以异步的方式暴露出来,特别是当它们要进行I/O操作或者庞大的计算处理。再举个例子,获取数据的API应该以异步的方式实现,而不应该像这样:
// WRONG: this will make the UI freeze when getting the data
var data = getData();
alert("We got data: " + data);
这个API的设计需要getData()为阻塞,但这会冻结用户界面直到获取到了数据。如果数据在JS上下文的内部,这个影响微乎其微,但是如果是通过网络获取或者在SQLite或者index存储中,这将会对用户体验产生戏剧性的影响。
正确的API设计是主动地让所有程序API能够执行一段时间,一开始就异步,因为将同步的代码改造为异步的代码会是一个艰巨的任务。
例如,简单化的getData() API应该像这样:
getData(function(data){
alert("We got data: " + data);
});
这样的好处是这会让应用程序的UI代码一开始以异步为中心,然后允许相关的API决定未来是否需要异步或同步。
注意,并不是所有的应用程序API都需要或者应该异步。判定准则是任何API操作I/O或者处理大型数据(超过15毫秒)都应该一开始就异步,即使第一个的实现是同步的。
失败处理
传统的方式是使用try/catch来处理异步编程中的错误,但是这种方式并不是真正的奏效,因为错误通常发生在另一个线程中。所以,当前执行的函数需要一个结构化的方式来通知上一级的函数当在处理过程中发生了错误。
在基于事件的异步API中,当接受事件时,通常是根据应用程序代码查询事件或者对象来完成的。而基于回调的异步API中,最好的实践方式是给第二个参数传入错误时需要执行的函数。
我们的getData应该像这样:
// getData(successFunc,failFunc);
getData(function(data){
alert("We got data: " + data);
}, function(ex){
alert("oops, some problem occured: " + ex);
});
和$.Deferred结合使用
上述回调的一个限制是当我们写同步逻辑的时候,代码将会变得很繁琐。
例如,如果你需要等待两个异步的API完成采取做第三件事时,代码复杂度将会立刻提升:
// first do the get data.
getData(function(data){
// then get the location
getLocation(function(location){
alert("we got data: " + data + " and location: " + location);
},function(ex){
alert("getLocation failed: " + ex);
});
},function(ex){
alert("getData failed: " + ex);
});
当多个程序需要调用相同的回调时,事件会变得更加复杂。因为每次调用将必须执行这些多步骤调用,否则应用程序将不得不实施其自己的缓存机制。
很幸运,有一种相关的旧的模式叫做Promises(类似于Java的Future),jQuery核心提供了一个健壮的现代实现,$.Deferred针对异步编程提供了一个简单而又强大的解决方案。
为了简便,Promises模式定义了异步API返回一个Promise对象,上一级函数获得Promise对象,然后执行done(successFunc(data)),告诉Promise对象当data解决(获取)了,就执行successFunc这个函数。
例子:
// get the promise object for this API
var dataPromise = getData();
// register a function to get called when the data is resolved
dataPromise.done(function(data){
alert("We got data: " + data);
});
// register the failure function
dataPromise.fail(function(ex){
alert("oops, some problem occured: " + ex);
});
// Note: we can have as many dataPromise.done(...) as we want.
dataPromise.done(function(data){
alert("We asked it twice, we get it twice: " + data);
});
这里我们首先获得了dataPromise对象,然后调用.done方法来注册一个当数据得到解决我们想要执行的函数。我们也可以调用.fail方法来处理错误结果。我们还可以执行更多我们需要的.done或者.fail方法,因为jQuery相关的Promise实现会处理注册和回调。
有了这种模式,现在想要实现更高级的异步代码就更简单了,而且jQuery已经提供了非常常用的$.when这个方法。
举个例子,上面被嵌套的getData/getLocation 回调应该像这样:
// assuming both getData and getLocation return their respective Promise
var combinedPromise = $.when(getData(), getLocation())
// function will be called when both getData and getLocation resolve
combinePromise.done(function(data,location){
alert("We got data: " + data + " and location: " + location);
});
使用jQuery.Deferred的美妙之处在于开发者可以很轻松的实现异步函数,例如像这样的getData:
function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();
// ---- AJAX Call ---- //
var xhr = new XMLHttpRequest();
xhr.open("GET","data",true);
// register the event handler
xhr.addEventListener('load',function(){
if(xhr.status === 200){
// 3.1) RESOLVE the DEFERRED (this will trigger all the done()...)
deferred.resolve(xhr.response);
}else{
// 3.2) REJECT the DEFERRED (this will trigger all the fail()...)
deferred.reject("HTTP error: " + xhr.status);
}
},false)
// perform the work
xhr.send();
// Note: could and should have used jQuery.ajax.
// Note: jQuery.ajax return Promise, but it is always a good idea to wrap it
// with application semantic in another Deferred/Promise
// ---- /AJAX Call ---- //
// 2) return the promise of this deferred
return deferred.promise();
}
当getData()被调用时,它一开始会创建一个新的jQuery.Deferred对象(1),然后返回它的Promise对象(2),这样getData就可以注册它的done和fail方法。接着,当XHR执行返回,它既可能解决这个deferred(3.1)也可能拒绝它(3.2)。当解决deferred时将会触发所有done函数和其他的promise函数(例如then和pipe),拒绝deferred则会执行fail函数。
使用场景:
数据访问: 在远程数据访问时,异步远程调用将会很明显的影响用户体验。而且在本地数据中像低级的API(SQLite和IndexedDB)本身就是异步的。Deferred API的 $.when和 .pipe在同步和链式异步子查询时非常强大。
UI动画: 编写一个或多个动画的transitionEnd事件是一件非常乏味的事,特别是当动画师CSS3动画和JS的混合。将animarion函数用Deferred包装起来可以显著地减少代码复杂度和提升灵活性。甚至一个简单通用的包装器函数像cssAnimation(className)返回一个Promise对象(当transitionEnd时得到解决)都将受益匪浅。
UI组件显示: 这个有点高级,但是高级的HTML组件框架应该也使用Deferred。当一个应用程序需要显示不同部分的用户界面,将组件通过Deferred封装可以更大的控制生命周期。
任何的异步API: 将浏览器API通过Deferred包装,在字面上一个只用加4-5行代码,但却会极大地简化应用程序代码。
缓存化: 这是一种附带的好处,但在一些场合里这会非常有用。在异步调用时可将Deferred 对象缓存起来。好处是调用者不需要知道调用是否被解决或者正在被解决中,它的回调函数将会完全以相同的方式被调用。
结论
$.Deferred概念很简单,但是需要一段时间才能掌握好它。掌握js异步编程对任何HTML5应用程序开发者而言是很必要的,而且Promise模式使异步编程更可靠和强大!