50万奖金+官方证书,深圳国际金融科技大赛正式启动,点击报名 了解详情
写点什么

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:372448

评论

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

企业数字化转型之路,从这里开始

天翼云开发者社区

数字化转型 云存储

AI金榜题名时,MLPerf榜单的份量究竟有多重?

脑极体

XaaS 陷阱:万物皆服务(可能)并不是IT真正需要的东西

雨果

云服务 xaas DaaS 本地服务

MMAP

北洋

Andriod 7月月更

不要再手动批量替换了,使用python AST模块批量替换

阿呆

Python AST 批量替换

Spring你牛个啥,我承认刚才说话我声音有点大

zxhtom

7月月更

华为云ModelArts文本分类–外卖评论

逝缘~

深度学习 华为云 7月月更

Java方向~~0基础小白如何快速脱离0offer的苦海!

KEY.L

7月月更

刷个算法,结果第一题就蚌埠住了~~

为自己带盐

算法 力扣 7月月更

7000+字图文并茂解带你深入理解java锁升级的每个细节

华为云开发者联盟

Java 开发 华为云

中文版Postman?功能真心强大!

Liam

Java 开发者工具 Postman 后端开发 程序员进阶

一文读懂简单查询代价估算

华为云开发者联盟

数据库 后端 查询引擎

场景化面试:关于分布式锁的十问十答

面试官问

分布式锁

如何开发引入小程序插件

Geek_99967b

小程序插件

一朵云开启智慧交通新未来

天翼云开发者社区

区块链 大数据 物联网

鱼和熊掌可以兼得!天翼云弹性裸金属一招鲜!

天翼云开发者社区

服务器 弹性扩容

集合处理的利器

技术小生

java8 7月月更

Ubuntu 20.04 安装 Chisel

贾献华

7月月更

开创人工智能产业新未来!7月8日昇思生态论坛与你相约广州

科技热闻

使用 RepositoryProvider简化父子组件的传值

岛上码农

flutter ios 安卓 移动端开发 7月月更

牛客java选择题每日打卡Day7

京与旧铺

7月月更

从 1.5 开始搭建一个微服务框架——调用链追踪 traceId

悟空聊架构

日志 链路追踪 traceId 悟空聊架构 7月月更

如何组织一场实战攻防演练

穿过生命散发芬芳

攻防演练 7月月更

微服务链路风险分析

阿泽🧸

7月月更 链路风险分析

【愚公系列】2022年7月 Go教学课程 004-Go代码注释

愚公搬代码

7月月更

让开发效率飞速提升的跨端方案

Geek_99967b

小程序 跨端 小程序容器

分布式算法入门之 Paxos 算法

宇宙之一粟

Basic paxos 7月月更

国内低代码开发平台靠谱的都有哪些?

AIRIOT

低代码 物联网 低代码,项目开发

【刷题记录】1. 两数之和

WangNing

7月月更

systemd-resolved 开启 debug 日志

程序员与厨子

ubuntu 运维 DNS systemd-resolved

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