写点什么

ASYNC/AWAIT 能够让代码更加简洁

  • 2018-08-16
  • 本文字数:6200 字

    阅读完需:约 20 分钟

本文最初发布于 Patrick Triest 的个人技术博客,经原作者授权由InfoQ 中文站翻译并分享。

我是如何放弃编写回调函数并爱上JavaScript ES8 的

现代的JavaScript 项目有时候会面临失控的危险。其中有个主要的原因就是处理异步任务中的混乱,它们会导致冗长、复杂和深度嵌套的代码块。JavaScript 现在为这种操作提供了新的语法,它甚至能够将最复杂的异步操作转换成简洁且具有高度可读性的代码。

背景

AJAX(异步 JavaScript 与 XML)

首先,我们来回顾一下历史。在 20 世纪 90 年代,在异步 JavaScript 方面,Ajax 是第一个重大突破。这项技术允许 Web 站点在 HTML 加载完之后,拉取和展现新的数据,当时,大多数的 Web 站点为了进行内容更新,都会再次下载整个页面,因此这是一个革命性的理念。这项技术(因为 jQuery 中打包了辅助函数使其得以流行开来)主导了本世纪前十年的 Web 开发,如今,Ajax 是目前 Web 站点用来获取数据的主要技术,但是 XML 在很大程度上被 JSON 所取代了。

Node.js

当 Node.js 在 2009 年首次发布时,服务器环境的主要关注点在于允许程序优雅地处理并发。当时,大多数的服务器端语言通过 _ 阻塞 _ 代码执行的方式来处理 I/O 操作,直到操作完成为止。NodeJS 却采用了事件轮询的架构,这样的话,开发人员可以设置“回调(callback)”函数,该函数会在 _ 非阻塞 _ 的异步操作完成之后被调用,这与 Ajax 语法的工作原理是类似的。

Promise

几年之后,在 Node.js 和浏览器环境中都出现了一个新的标准,名为“Promise”,它提供了强大且标准的方式来组合异步操作。Promise 依然使用基于回调的格式,但是提供了一致的语法来链接(chain)和组合异步操作。Promise 最初是由流行的开源库所倡导的库,在 2015 年最终作为原生特性添加到了 JavaScript 中。

Promise 是一项重要的功能改善,但它们依然经常会产生冗长且难以阅读的代码。

现在,我们有了一种解决方案。

Async/await 是一种新的语法(借鉴自.NET and C#),它允许我们在组合 Promise 时,就像正常的同步函数那样,不需要使用回调。对于 JavaScript 语言来说,这是非常棒的新特性,它是在 JavaScript ES7 中添加进来的,能够用来极大地简化已有的 JS 应用程序。

样例

接下来,我们将会介绍几个代码样例。

这里并不需要其他的库。在最新的 Chrome、Firefox、Safari 和 Edge 中, async/await 已经得到了完整的支持,所以你可以在浏览器的控制台中尝试这些样例。另外,async/await 能够用于 Node.js 7.6 及以上的版本,而且 Babel 和 Typescript 转译器也支持该语法,所以现在它能够用到任意的 JavaScript 项目中。

搭建

如果你想要在自己的机器上跟着运行这些代码的话,那么将会用到这个虚拟的 API 类。这个类模拟网络调用,返回 Promise,这个 Promise 将会在调用 200ms 之后以简单示例数据的方式完成处理。

复制代码
class Api {
constructor () {
this.user = { id: 1, name: 'test' }
this.friends = [ this.user, this.user, this.user ]
this.photo = 'not a real photo'
}
getUser () {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.user), 200)
})
}
getFriends (userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.friends.slice()), 200)
})
}
getPhoto (userId) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(this.photo), 200)
})
}
throwError () {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Intentional Error')), 200)
})
}
}

每个样例都会按顺序执行三个相同的操作:检索某个用户、检索他们的好友、获取他们的图片。最后,我们会将所有的三个结果打印在控制台上。

第一次尝试:嵌套 Promise 回调函数

下面的代码展现了使用嵌套 Promise 回调函数的实现。

复制代码
function callbackHell () {
const api = new Api()
let user, friends
api.getUser().then(function (returnedUser) {
user = returnedUser
api.getFriends(user.id).then(function (returnedFriends) {
friends = returnedFriends
api.getPhoto(user.id).then(function (photo) {
console.log('callbackHell', { user, friends, photo })
})
})
})
}

看上去,这似乎与我们在 JavaScript 项目中的做法非常类似。要实现非常简单的功能,结果代码块变得非常冗长且具有很深的嵌套,结尾处的代码甚至变成了这种样子:

复制代码
})
})
})
}
{1}

在真实的代码库中,每个回调函数可能会非常长,这可能会导致庞大且深层交错的函数。处理这种类型的代码,在回调中继续使用回调,就是通常所谓的“回调地狱”。

更糟糕的是,这里没有错误检查,所以其中任何一个回调都可能会悄无声息地发生失败,表现形式则是未处理的 Promise 拒绝。

第二次尝试:Promise 链

接下来,我们看一下是否能够做得更好一些。

复制代码
function promiseChain () {
const api = new Api()
let user, friends
api.getUser()
.then((returnedUser) => {
user = returnedUser
return api.getFriends(user.id)
})
.then((returnedFriends) => {
friends = returnedFriends
return api.getPhoto(user.id)
})
.then((photo) => {
console.log('promiseChain', { user, friends, photo })
})
}

Promise 非常棒的一项特性就是它们能够链接在一起,这是通过在每个回调中返回另一个 Promise 来实现的。通过这种方式,我们能够保证所有的回调处于相同的嵌套级别。我们在这里还使用了箭头函数,简化了回调函数的声明。

这个变种形式显然比前面的更易读,也更加具有顺序性,但看上去依然非常冗长和复杂。

第三次尝试:Async/Await

在编写的时候怎样才能避免出现回调函数呢?这难道是不可能实现的吗?怎样使用 7 行代码完成编写呢?

复制代码
async function asyncAwaitIsYourNewBestFriend () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const photo = await api.getPhoto(user.id)
console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}

这样就更好了。在返回 Promise 函数调用之前,添加“await”将会暂停函数流,直到 Promise 处于 resolved 状态为止,并且会将结果赋值给等号左侧的变量。借助这种方式,我们在编写异步操作流时,能够像编写正常的同步命令序列一样。

我希望,此时你能像我一样感到兴奋。

注意:“async”要放到函数声明开始的位置上。这是必须的,它实际上会将整个函数变成一个 Promise,稍后我们将会更深入地对其进行介绍。

LOOPS

使用 async/await 能够让很多在此之前非常复杂的操作变得很简便。例如,如果我们想要顺序地获取某个用户的好友的好友,那该怎么实现呢?

第一次尝试:递归 Promise 循环

如下展现了如何通过正常的 Promise 按顺序获取每个好友列表:

复制代码
function promiseLoops () {
const api = new Api()
api.getUser()
.then((user) => {
return api.getFriends(user.id)
})
.then((returnedFriends) => {
const getFriendsOfFriends = (friends) => {
if (friends.length > 0) {
let friend = friends.pop()
return api.getFriends(friend.id)
.then((moreFriends) => {
console.log('promiseLoops', moreFriends)
return getFriendsOfFriends(friends)
})
}
}
return getFriendsOfFriends(returnedFriends)
})
}

我们创建了一个内部函数,该函数会以 Promise 链的形式递归获取好友的好友,直至列表为空为止。它完全是函数式的,这一点非常好,但对于这样一个非常简单的任务来说,这个方案依然非常复杂。

注意:如果希望通过Promise.all()来简化promiseLoops()函数的话,将会导致明显不同的函数行为。本例的意图是展示顺序操作(每次一个),而Promise.all()用于并发(所有操作同时)运行异步操作。Promise.all()与 async/await 组合使用会有很强的威力,我们在下面的章节中将会进行讨论。

第二次尝试:Async/Await For 循环

采用 Async/Await 之后看起来就容易多了:

复制代码
async function asyncAwaitLoops () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
for (let friend of friends) {
let moreFriends = await api.getFriends(friend.id)
console.log('asyncAwaitLoops', moreFriends)
}
}

此时,我们不需要编写任何的递归 Promise 闭包。只需一个 for-loop 即可,所以 async/await 是能够帮助我们的好朋友。

并行操作

按照一个接一个的顺序获取每个好友似乎有些慢,为什么不用并行的方式来进行操作呢?借助 async/await 能够实现这一点吗?

是的,当然可以。它解决了我们所有的问题。

复制代码
async function asyncAwaitLoopsParallel () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const friendPromises = friends.map(friend => api.getFriends(friend.id))
const moreFriends = await Promise.all(friendPromises)
console.log('asyncAwaitLoopsParallel', moreFriends)
}

要并行运行操作,首先生成一个要运行的 Promise 的列表,然后将其作为参数传递给Promise.all()。这样会返回一个 Promise 让我们去 await 它完成,当所有的操作都结束时,它就会进行 resolve 处理。

错误处理

在异步编程中,还有一个主要的问题我们没有解决,那就是错误处理。它是很多代码库的软肋,异步错误处理一般要涉及到为每个操作编写错误处理的回调。将错误传递到调用堆栈的顶部可能会非常复杂,通常需要在每个回调开始的地方显式检查是否有错误抛出。这种方式冗长繁琐并且容易出错。此外,如果没有恰当地进行处理,Promise 中抛出的异常将导致悄无声息地失败,这会产生代码库中错误检查不全面的“不可见的错误”。

我们再看一下样例,为它们依次添加错误处理功能。为了测试错误处理,我们在获取用户的照片之前,将会调用一个额外的函数,“api.throwError()”。

第一次尝试:Promise 错误回调

我们首先看一个最糟糕的场景。

复制代码
function callbackErrorHell () {
const api = new Api()
let user, friends
api.getUser().then(function (returnedUser) {
user = returnedUser
api.getFriends(user.id).then(function (returnedFriends) {
friends = returnedFriends
api.throwError().then(function () {
console.log('Error was not thrown')
api.getPhoto(user.id).then(function (photo) {
console.log('callbackErrorHell', { user, friends, photo })
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}, function (err) {
console.error(err)
})
}

这是非常恐怖的一种写法。除了非常冗长和丑陋之外,控制流非常不直观,因为它是从输出接入的,而不像正常的、易读的代码库那样,从顶部到底部进行编写。

第二次尝试:Promise 链的“Catch”方法

我们可以使用 Promise 的“catch”方法,对此进行一些改善。

复制代码
function callbackErrorPromiseChain () {
const api = new Api()
let user, friends
api.getUser()
.then((returnedUser) => {
user = returnedUser
return api.getFriends(user.id)
})
.then((returnedFriends) => {
friends = returnedFriends
return api.throwError()
})
.then(() => {
console.log('Error was not thrown')
return api.getPhoto(user.id)
})
.then((photo) => {
console.log('callbackErrorPromiseChain', { user, friends, photo })
})
.catch((err) => {
console.error(err)
})
}

比起前面的写法,这当然更好一些了,我们在 Promise 链的最后使用了一个 catch 函数,这样能够为所有的操作提供一个错误处理器。但是,这还有些复杂,我们还是需要使用特定的回调来处理异步错误,而不能像处理正常的 Javascript 错误那样来进行处理。

第三次尝试:正常的 Try/Catch 代码块

我们可以更进一步。

复制代码
async function aysncAwaitTryCatch () {
try {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
await api.throwError()
console.log('Error was not thrown')
const photo = await api.getPhoto(user.id)
console.log('async/await', { user, friends, photo })
} catch (err) {
console.error(err)
}
}

在这里,我们将整个操作包装在了一个正常的 try/catch 代码块中。通过这种方式,我们可以按照完全相同的方式,抛出和捕获同步代码和异步代码中的错误。这种方式简单了很多。

组合

我在前面的内容中曾经提到过,带有“async”标签的函数实际上会返回一个 Promise。这样的话,就允许我们非常容易地组合异步控制流。

例如,我们可以重新配置前面的样例,让它返回用户数据,而不是简单地打印日志。我们可以通过调用 async 函数,将其作为一个 Promise 来获取数据。

复制代码
async function getUserInfo () {
const api = new Api()
const user = await api.getUser()
const friends = await api.getFriends(user.id)
const photo = await api.getPhoto(user.id)
return { user, friends, photo }
}
function promiseUserInfo () {
getUserInfo().then(({ user, friends, photo }) => {
console.log('promiseUserInfo', { user, friends, photo })
})
}

更好的一点在于,我们可以在接收者函数中使用 async/await 语法,这样的话,就能形成完全具有优势、非常简单的异步编程代码块。

复制代码
async function awaitUserInfo () {
const { user, friends, photo } = await getUserInfo()
console.log('awaitUserInfo', { user, friends, photo })
}

如果我们想要获取前十个用户的数据,那又该怎样处理呢?

复制代码
async function getLotsOfUserData () {
const users = []
while (users.length < 10) {
users.push(await getUserInfo())
}
console.log('getLotsOfUserData', users)
}

如果想要并行该怎么办呢?怎样添加完备的错误处理功能?

复制代码
async function getLotsOfUserDataFaster () {
try {
const userPromises = Array(10).fill(getUserInfo())
const users = await Promise.all(userPromises)
console.log('getLotsOfUserDataFaster', users)
} catch (err) {
console.error(err)
}
}

结论

随着单页 JavaScript Web 应用的兴起和 Node.js 的广泛采用,对于 JavaScript 开发人员来说,优雅地处理并发变得比以往更加重要。async/await 能够缓解很多易于引入缺陷的控制流问题,这些问题已经困扰 JavaScript 代码库许多年了。同时,它还能确保异步代码块更加简短、更加简洁、更加清晰。随着主流浏览器和 Node.js 的广泛支持,现在是一个非常好的时机将其集成到你自己的代码实践和项目之中。

感谢徐川对本文的审校。

2018-08-16 15:372097

评论

发布
暂无评论
发现更多内容

Linux 操作系统!开篇!!!

苹果看辽宁体育

Linux

模式与重构-作业

秤须苑

「NIO系列」——之Reactor模型

小谈

Spring Boot reactor 后端 nio SpringCloud

终于有大佬把TCP/IP协议讲清楚了!面试再也不怂面试官提问了

小闫

jdk JVM Netty buffer TCP/IP

Google官方MVP+Dagger2架构详解

小吴选手

架构 架构师 架构是训练营

再有人问你分布式事务,把这篇扔给他

码哥小胖

分布式 Java 分布式

去面试Spring Cloud 被问的35个问题

小谈

面试 springboot SpringCloud buffer JVM原理

谈谈容器和K8s

Gabriel

腾讯的辣酱不香了 支付宝的区块链真能解决“萝卜章”问题?

CECBC

双链通 萝卜章 区块链方案

写给孩子的两本书我读得津津有味

孙苏勇

读书 陪伴 随笔杂谈

如果是你,年薪80万和阿里P7月薪36K,会怎么选?

犬来八荒

Java 腾讯 面试 阿里

基于 Flagger 和 Nginx-Ingress 实现金丝雀发布

郭旭东

Kubernetes CI/CD

如何快速将 Linux 系统制作成 ISO 镜像文件?

JackTian

Linux 运维 操作系统 镜像文件 ISO

产业数字化无法“一蹴而就”,而是“长跑冠军”。

CECBC

被“假”老干妈耍惨了?憨憨腾讯花1624万卖萌,引全网吃瓜!

程序员生活志

腾讯 互联网 大厂

最详细的 Spring Cloud OAuth2 单点登录使用教程送给大家

小闫

面试 后端 JVM SpringCloud

Java面试常用知识(附赠最新面试题)

架构大数据双料架构师

使用 Flutter 快速实现请假与写周报应用

LeanCloud

flutter 后端 数据 教程

当国产iVX遇上新晋产品PowerPlatform,能否披荆斩棘、稳住阵脚?

代码制造者

程序员 编辑器 低代码 快速开发 开发工具

游戏夜读 | 关卡设计新手必看

game1night

【自学成才系列一】multipass安装篇

小朱

multipass

信创舆情一线--英特尔暂停向浪潮供货

统小信uos

服务器 舆情 芯片

拥抱开源开放,易观技术开发者的星海征途

易观大数据

海豚调度 调度引擎

架构0期Week4作业1

Nan Jiang

极客大学架构师训练营 系统架构 分布式缓存 一致性哈希 Hash 第9课 听课总结

John(易筋)

极客时间 极客大学 极客大学架构师训练营 分布式缓存 一致性哈希

到底什么是HashMap?

小闫

Java spring 后端 JVM hashmap

七月份最新“美团+字节+腾讯”面试题,测试一下你能走到哪一面?

犬来八荒

Java 面试 线程’

ARTS-week5

王钰淇

ARTS 打卡计划

架构师训练营第五周总结

Melo

极客大学架构师训练营

面试官:十亿级数据ES搜索怎么优化?我直接傻了

犬来八荒

Java 面试 大厂

架构0期Week4作业2

Nan Jiang

ASYNC/AWAIT能够让代码更加简洁_JavaScript_Patrick Triest_InfoQ精选文章