Firefox 中引入 Async/Await

新的asyncawait关键字——使异步代码更简洁、更易懂、更易维护——已在 Firefox 52 中推出。目前可在最新的开发者版中使用,Firefox 52 计划于 2017 年 3 月正式发布。

JavaScript 在 Web 上出色的单线程性能和响应能力归功于其普遍的异步设计。不幸的是,同样的设计也导致了“回调地狱”,其中对异步函数的顺序调用需要深度嵌套、难以管理的代码,如下面的使用localforage库的稍微人为设计的示例所示

function foo(callback) {
  localforage.setItem('x',  Math.random(), function(err) {
    if (err) {
      console.error("Something went wrong:", err);
    } else {
      localforage.getItem('x', function(err, value) {
        if (err) {
          console.error("Something went wrong:", err);
        } else {
          console.log("The random number is:", value);
        }

        if (callback) {
          callback();
        }
      });
    }
  });
}

foo(function() { console.log("Done!"); });

如果您跳过了这段代码,或者没有立即理解它的作用,那就是问题所在

ES2015 开始通过将Promise标准化为链接的异步函数来解决此问题。自从引入以来,Promise 已成为新的 Web 标准(包括fetch服务工作线程)不可或缺的一部分。它们使得可以将前面的示例重写为

function foo() {
  return localforage.setItem('x', Math.random())
         .then(() => localforage.getItem('x'))
         .then((value) => console.log("The random number is:", value))
         .catch((err) => console.error("Something went wrong:", err));
}

foo().then(() => console.log("Done!"));

由于有了 Promise,代码不会随着每次连续调用而嵌套得更深,并且所有错误处理都可以在链的末尾合并到一个案例中。

请注意,在上面的示例中,foo()localforage 完成其工作之前立即返回。因为foo()本身返回一个 Promise,所以可以在它使用.then()方法完成之后安排未来的回调。

从语义上讲,上面的示例更加简单明了,但在语法上,仍然有很多内容需要阅读和理解。新的asyncawait关键字是语法糖,建立在 Promise 之上,有助于使 Promise 更易于管理

async function foo() {
  try {
    await localforage.setItem('x', Math.random());
    let value = await localforage.getItem('x');
    console.log("The random number is:", value);
  } catch (err) {
    console.error("Something went wrong:", err);
  }
}

foo().then(() => console.log("Done!"));

上面的代码在功能上与前面的示例相同,但更容易理解和维护,因为函数体现在类似于常见的同步函数。

标记为async的函数始终返回 Promise,因此对.then()的调用在其返回值上工作以安排回调。以await为前缀的表达式有效地暂停函数,直到表达式解析。如果awaited 表达式遇到错误,则执行传递到catch块。如果未捕获,则返回的 Promise 将进入拒绝状态。

类似地,而不是在async函数内部处理错误,也可以在返回值上使用普通的.catch()方法

async function foo() {
    await localforage.setItem('x', Math.random());
    let value = await localforage.getItem('x');
    console.log("The random number is:", value);
}

foo().catch(err => console.error("Something went wrong:", err))
     .then(() => console.log("Done!"));

对于更实际的示例,请考虑您可能编写的用于取消用户Web 推送通知订阅的函数

function unsubscribe() {
  return navigator.serviceWorker.ready
         .then(reg => reg.pushManager.getSubscription())
         .then(subscription => subscription.unsubscribe())
         .then(success => {
           if (!success) {
             throw "unsubscribe not successful";
           }
         });
}

使用asyncawait,它变成

async function unsubscribe() {
  let reg = await navigator.serviceWorker.ready;
  let subscription = await reg.pushManager.getSubscription();
  let success = await subscription.unsubscribe();
  if (!success) {
    throw "unsubscribe not successful";
  }
}

两者功能相同,但后一个示例隐藏了 Promise 的复杂性,并将异步代码转换为像同步代码一样读取(和执行)的代码:从上到下,等待每行代码完全解析,然后才能转到下一行。

asyncawait关键字的原生跨浏览器支持仍然处于起步阶段,但您可以借助 JavaScript 转译器(如Babel)在今天使用它们,该转译器可以将async / await转换为功能上等效的向后兼容代码。

要详细了解asyncawait关键字或 Promise,请查看以下资源

请记住,asyncawait只是 Promise 的帮助程序:您可以混合和匹配任何语法,并且您了解的所有关于 Promise 的知识都直接适用于asyncawait

特别感谢Jamund Ferguson建议改进本文中代码示例。

关于 Dan Callahan

Mozilla 开发者关系工程师,前 Mozilla Persona 开发人员。

更多 Dan Callahan 的文章…


10 条评论

  1. CrystalGamma

    但是请注意,async/await 用于构建异步的执行序列链。
    如果您想利用并发请求来减少延迟,您仍然需要使用其他机制,如 Promise 和回调。
    幸运的是,由于 async/await 只是 Promise 的语法糖,因此互操作性看起来非常轻松。

    2016 年 12 月 7 日 12:06

    1. Dan Callahan

      很好的观点!因为async/await只是 Promise 的语法糖,所以您可以将await与更低级的并发方法(如Promise.all()Promise.race())一起使用,并且这些函数可以接受async函数作为参数。

      2016 年 12 月 7 日 12:23

      1. voracity

        Promise.all() 不能有自己的语法糖吗?例如

        for (let v of await promises) {

        }

        这将表示

        Promise.all(promises).then(function(values) {
        for (let v of values) {

        }
        return nextPromiseIfThereIsOne;
        })

        因为我一直在等待 await(很长时间),所以我会尽可能避免使用 promise,所以如果代码不正确,请原谅我。

        我想 Promise.race() 将是一种高度专业化的需求,因此不需要自己的语法糖。

        2016 年 12 月 9 日 22:28

        1. Dan Callahan

          确实曾经有一个关于await* [...]的提案,它将被转换为await Promise.all([...]),但它在规范最终确定之前就被删除了。目前,我们仅限于更明确的Promise.all(),这在可用性/可发现性方面可能不是一件坏事。

          2016 年 12 月 12 日 09:13

  2. Matt Gale

    现在浏览器开始支持它真是太好了。我个人认为 Promise 比 async/await 更易于阅读,所以我不会切换。不过,拥有选择权很好。

    2016 年 12 月 8 日 03:50

    1. Dan Callahan

      绝对使用适合您的方法,但不要忘记,如果事情变得混乱,您可以切换到async函数。尤其是在您需要在一个函数中引用多个 Promise 的结果的情况下,例如我们有一个关于 Promise 的问题上的“高级错误 #4”,async函数使拥有有用的变量作用域变得容易得多。

      2016 年 12 月 8 日 10:23

      1. Matt Gale

        了解了。谢谢

        2016 年 12 月 8 日 13:49

  3. Mindaugas J

    您知道还有什么可以在函数中间暂停执行吗?

    生成器。

    代码看起来基本相同,只需将 await 替换为 yield,将 function* 替换为 async function 即可。您只需要一个好的生成器运行器。好处是,所有当前浏览器都支持它们。

    无论如何,我已经尝试在节点中编写 async/await 代码(使用`—harmony_async_await`),并且我再也不会使用回调/promise 汤了。

    2016 年 12 月 8 日 12:13

    1. Dan Callahan

      您说得对,生成器可用于解决缺少对异步函数的支持:这正是 Babel 的transform-async-to-generator插件所做的。但是,在 IE 或旧版 Safari 版本上使用生成器需要相对重量级的Regenerator polyfill。它有效,但这是一种解决方法,并且增加了不必要的复杂性。出于这个原因,Koa Web 框架在 Node.js 中可用后,将从生成器切换到异步函数,并且像fast-async这样的项目正在 Babel 中实现异步函数的替代转译方法。

      2016 年 12 月 8 日 15:14

  4. Andrei

    对我来说,Async await 是新 JS 中最令人兴奋的事情。

    那些说“我对 Promise 满意”的人没有编写足够的异步代码来真正理解 async await。Async await 很棒,因为它使编写和维护异步代码变得更容易,而这正是我们的大脑不太擅长的事情。

    2016 年 12 月 11 日 09:12

本文评论已关闭。