BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

承诺特性:在浏览器中使用JavaScript进行异步工作的新标准?

| 作者 Zef Hemel 关注 0 他的粉丝 ,译者 李彬 关注 1 他的粉丝 发布于 2014年5月6日. 估计阅读时间: 11 分钟 | Google、Facebook、Pinterest、阿里、腾讯 等顶尖技术团队的上百个可供参考的架构实例!

每个使用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);
    }
});

这些模式都需要面对一些挑战。例如:

  • 需要手动进行错误传播。在异步编程风格中,我们可以使用throw、try和catch来处理和传播错误。这些语言层面的机制无法良好地应对异步流。更糟糕的是,忘记正确地处理错误,将很容易产生错误——从而令进程消失或崩溃。
  • 回调函数永远不会得到执行,或是被多次调用。当我们编写异步函数的时候,往往会意外地忘记调用回调函数,或是调用多次。两种情况都会让调试问题变得非常困难。
  • 回调函数天生会带来深度嵌套的回调。这是可以避免的,但是在异步编码中这也确实是常见问题。

承诺特性(Promises)

承诺特性的目标是简化异步代码的编写。使用承诺特性的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,有一些需要注意的事情:

  • 嵌套只有一层。
  • 错误处理集中在一处。如果某个错误发生在第一次调用时,它将被传播直到then调用处理它。

另外,承诺还有许多其他优点,可以查看文章最后给出的其他阅读材料的链接。

未来

在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?

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT