异步 JavaScript 编程 $.Deferred 对 HTML5 应用程序的强大功能
构建流畅且响应迅速的 HTML5 应用程序最重要的方面之一是应用程序所有不同部分之间的同步,例如数据获取、处理、动画和用户界面元素。
与桌面或本机环境的主要区别是浏览器不提供对线程模型的访问权限,而是为访问用户界面(即 DOM)的所有内容提供单个线程。 这意味着访问和修改用户界面元素的所有应用程序逻辑始终在同一个线程中,因此保持所有应用程序工作单元尽可能小和高效以及尽可能多地利用浏览器提供的任何异步功能的重要性可能的。
浏览器异步 API
幸运的是,浏览器提供了许多异步 API,例如常用的 XHR(XMLHttpRequest 或“AJAX”)API,以及 IndexedDB、SQLite、HTML5 Web worker 和 HTML5 GeoLocation API 等等。 甚至一些与 DOM 相关的操作也是异步公开的,例如通过 transitionEnd 事件的 CSS3 动画。
浏览器向应用程序逻辑公开异步编程的方式是通过事件或回调。 在基于事件的异步 API 中,开发人员为给定对象(例如 HTML 元素或其他 DOM 对象)注册一个事件处理程序,然后调用该操作。 浏览器通常会在不同的线程中执行操作,并在适当的时候在主线程中触发事件。
例如,使用基于事件的异步 API XHR 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();
CSS3 transitionEnd 事件是基于事件的异步 API 的另一个示例。
// 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 Geolocation,是基于回调的,这意味着开发人员将函数作为参数传递,该函数将由具有相应分辨率的底层实现回调。
例如,对于 HTML5 Geolocation,代码如下所示:
// 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() 是阻塞的,这将冻结用户界面,直到获取数据为止。 如果数据在 JavaScript 上下文中是本地的,那么这可能不是问题,但是如果数据需要从网络中获取,甚至是在本地 SQLite 或索引存储中,这可能会对用户体验产生巨大影响。
正确的设计是主动让所有可能需要一些时间来处理的应用程序 API 从一开始就异步,因为将同步应用程序代码改造为异步可能是一项艰巨的任务。
例如,简单的 getData() API 会变成类似这样的东西:
getData(function(data){
alert("We got data: " + data);
});
这种方法的好处在于,它强制应用程序 UI 代码从一开始就以异步为中心,并允许底层 API 在稍后阶段决定它们是否需要异步。
请注意,并非所有应用程序 API 都需要或应该是异步的。 经验法则是,任何执行任何类型的 I/O 或繁重处理(任何可能需要超过 15 毫秒的操作)的 API 都应该从一开始就异步公开,即使第一个实现是同步的。
处理失败
异步编程的一个问题是传统的 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 之前需要等待两个异步 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 对象 调用此successFunc 。
在“数据”被解析时
所以,上面的getData调用例子变成了这样:
// 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调用,因为底层 Promise 实现(jQuery 代码)将处理注册和回调。
请注意,我们可以根据需要
有了这个模式,实现更高级的同步代码就相对容易了,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: " + dataResult + " and location: " + location);
});
而这一切的美妙之处在于 jQuery.Deferred 使开发人员可以非常轻松地实现异步功能。 例如,getData 可能看起来像这样:
function getData(){
// 1) create the jQuery Deferred object that will be used
var deferred = $.Deferred();
// ---- AJAX Call ---- //
XMLHttpRequest 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),以便调用者可以注册其完成和失败函数。 然后,当 XHR 调用返回时,它会解析延迟的 (3.1) 或拒绝它 (3.2)。 执行 deferred.resolve 将触发所有 done(...) 函数和其他 promise 函数(例如 then 和 pipe),调用 deferred.reject 将调用所有 fail() 函数。
用例
以下是一些 Deferred 非常有用的用例:
数据访问: 将数据访问 API 公开为 $.Deferred 通常是正确的设计。 这对于远程数据来说是显而易见的,因为同步远程调用会完全破坏用户体验,但对于本地数据也是如此,因为较低级别的 API(例如,SQLite 和 IndexedDB)本身通常是异步的。 Deferred API 的 $.when 和 .pipe 非常强大,可以同步和链接异步子查询。
UI 动画: 使用 transitionEnd 事件编排一个或多个动画可能非常乏味,尤其是当动画是 CSS3 动画和 JavaScript 的混合时(通常是这种情况)。 将动画函数包装为 Deferred 可以显着降低代码复杂度并提高灵活性。 即使是像 cssAnimation(className) 这样简单的通用包装函数,它将返回在 transitionEnd 上解析的 Promise 对象,也可能会有很大帮助。
UI 组件显示: 这有点高级,但高级 HTML 组件框架也应该使用 Deferred。 无需过多讨论细节(这将是另一篇文章的主题),当应用程序需要显示用户界面的不同部分时,将这些组件的生命周期封装在 Deferred 中可以更好地控制时间。
任何浏览器异步 API: 出于规范化的目的,将浏览器 API 调用包装为 Deferred 通常是个好主意。 这实际上需要 4 到 5 行代码,但会大大简化任何应用程序代码。 如上面的 getData/getLocation 伪代码所示,这允许应用程序代码拥有一个跨所有类型 API(浏览器、应用程序特定和复合)的异步模型。
缓存: 这是一种附带的好处,但在某些情况下可能非常有用。 因为可以在执行异步调用之前或之后调用 Promise API(例如,.done(..) 和 .fail(..)),所以 Deferred 对象可以用作异步调用的缓存句柄。 例如,CacheManager 可以只跟踪给定请求的 Deferred,如果没有失效,则返回匹配的 Deferred 的 Promise。 美妙之处在于调用者不必知道调用是否已经被解决或正在被解决,其回调函数将以完全相同的方式被调用。
结论
虽然 $.Deferred 的概念很简单,但要很好地掌握它可能需要一些时间。 然而,鉴于浏览器环境的性质,掌握 JavaScript 中的异步编程对于任何认真的 HTML5 应用程序开发人员来说都是必须的,而 Promise 模式(和 jQuery 实现)是使异步编程可靠和强大的强大工具。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论