每个使用JavaScript进行进阶编程的人,可能都会遇到过异步编程:与立刻返回结果值的函数不同,在异步编程中我们传递了一个回调函数,它会在稍后某个时刻(当结果可用的时候)被调用。在JavaScript世界中,围绕着如何恰当地使用异步API构建大规模应用,一直存在着许多争论。然而,最近在ECMAScript6中增加了承诺特性的原生支持(目前最新版本的Chrome、Firefox和Opera已经提供了相应的支持),而且未来的浏览器API也将采纳它们。这是否会平息争论?承诺特性现在成为了浏览器环境中的JavaScript的“原生”部分,是否也会促使其成为异步应用代码编写的新标准?
什么是异步编程?举例来说,假定我们想要从某个Web页面中访问服务器获取mydata.json文件。如果读者对浏览器环境中的Web开发并不熟悉,那么或许心里会浮现起类似下面代码片段所示的一些API:
var result = http.get("/mydata.json"); console.log("Got result", result);
然而在Web中,这种写法要么不可实现,要么会被认为是糟糕的实践。其原因在于,基本上浏览器提供的是单线程运行环境:我们只有一个单一线程,它既负责渲染Web页面,又要负责处理事件并执行逻辑。因此,如果我们让这个线程完成开销很大或很缓慢的任务,那么在此期间浏览器就会陷入假死状态。也就是说,如果获取JSON文件需要两秒钟,那么这段时间里浏览器除了等待HTTP调用外,将无法做任何其他事情。这并不是很好的体验。为了避免这样的问题,JavaScript中大部分开销比较大的调用,都会以异步API的形式提供。
一般来说,JavaScript中异步方面的核心思想在于,不要阻塞主线程并让它等待响应,而是传递一个函数,在稍后(当数据文件被成功获取时)再调用它。因此,当HTTP调用正在进行的时候,浏览器可以继续去做其他事情。我们看一下经典的XMLHTTPRequest 例子:
var req = new XMLHttpRequest(); req.open("GET", "/mydata.json", true); req.onload = function(e) { var result = req.responseText; console.log("Got result", result); }; req.send();
这里的模式是创建一份请求对象,把一个事件监听器附加在上面,监听load事件。当它被触发时,我们可以继续之前的任务。在浏览器API中,带有事件发射器的请求对象模式颇为常见,但是也还有其他一些模式。例如,让我们看一下用来跟踪用户当前位置的Geolocation API:
navigator.geolocation.getCurrentPosition(function(result) { console.log("Location", result); }, function(err) { console.error("Got error", err); });
getCurrentPosition API接受多个回调函数,而不是返回一份请求对象。其中,当成功获得用户位置时,将调用第一个回调函数;而如果出现一些错误,则会调用第二个函数。在服务器端,在异步API中传入一个回调函数——这个回调函数接受两个参数(错误和结果)——作为最后一个参数的方式,事实上是node.js中的标准做法。例如下面这个是用node.js读取文件的例子:
var fs = require("fs"); fs.readFile("mydata.json", function(err, content) { if (err) { console.error("Got an error", err); } else { console.log("Got result", content); } });
这些模式都需要面对一些挑战。例如:
承诺特性的目标是简化异步代码的编写。使用承诺特性的API不接受回调参数,而是返回一个承诺对象。承诺对象只有少量的方法,其中最重要的是then(这也正是为何有时候承诺被称为“thenables”)。then方法接受一个或两个方法,第一个是当承诺被解决(成功)的时候被调用,而第二个则是承诺被拒绝(返回错误)的时候被调用。这两个回调函数都可以完成以下事情:
让我们看看下面这个例子。我们想要跟踪用户的位置,并使用该位置执行AJAX调用访问服务器。下面的代码采用了异步编程的常用风格,执行这一任务:
navigator.geolocation.getCurrentPosition(function(location) { var req = new XMLHttpRequest(); req.open("PUT", "/location", true); req.onload = function() { console.log("Posted location!"); }; req.onerror = function(e) { console.error("Putting failed", e); }; req.send(JSON.stringify(location.coords)); }, function(err) { console.error("Got error", err); });
如上所示,我们在这里拥有两个错误处理器。接下里再让我们看一个新的版本,它使用这些API基于承诺的假象的版本:
navigator.geolocation.getCurrentPosition().then(function(location) { var req = new XMLHttpRequest(); req.open("PUT", "/location", true); return req.send(JSON.stringify(location.coords)); }).then(function() { console.log("Posted location!"); }).then(null, function(err) { console.error("Got error", err); });
对于这些基于承诺的API,有一些需要注意的事情:
另外,承诺还有许多其他优点,可以查看文章最后给出的其他阅读材料的链接。
在Chrome 32、Firefox 29和Opera 19中,浏览器都使用了Promise构建器来集成这一特性。此外,未来的浏览器API将使用它们。一些例子包括:
随着承诺特性在浏览器API中变得越来越流行,它是否会被更多基于浏览器的应用和库采用?jQuery现在已经有了一套类似于承诺的机制,名为推迟(deferred)。此外,承诺甚至会从ECMAScript 6的生成器中受益更多,也即是:能够编写看起来像是同步风格但却异步执行的代码。
如果读者想要更多了解JavaScript中的全新原生承诺及其优点,Jake Archibald的HTML5Rocks文章可以作为一份良好的上手读物。另外还有一份小型兼容代码(polyfill),针对较老版本的浏览器。Domenic Denicola针对承诺特性做过许多很棒的演讲,而且长期以来一直是承诺的拥护者。关于承诺的API规范可以查看其所基于的the Promises/A+ proposal,以及Mozilla开发者网络。
查看英文原文:Promises: The New Async Standard in Browser JavaScript?