发布在即!企业 AIGC 应用程度测评,3 步定制专属评估报告。抢首批测评权益>>> 了解详情
写点什么

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

评论

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

中南财经政法大学教授施先旺:事项法会计促进业财合一和会计变革

用友BIP

技术大会 业财合一 业财融合 事项会计

实力担当!焱融文件存储再次中标中国移动项目

焱融科技

#高性能 #分布式文件存储 #文件存储 #中国移动

PCB为什么常用50Ω阻抗?6大原因

华秋PCB

科普 电路 阻抗 PCB PCB设计

软件测试/测试开发丨容器编排K8S 下部署分布式UI自动化解决方案

测试人

k8s 软件测试 #Kubernetes#

升级企业数智化底座,用友iuap助力企业高质量发展

用友BIP

用友 技术大会 iuap平台

被吐槽 GitHub仓 库太大,直接 600M 瘦身到 6M,这下舒服了

程序员小富

Java git

windows制作apple苹果证书-appuploader​

雪奈椰子

实践分享:如何在自己的App 中引入AI画图!

FN0

小程序 小程序容器 AI绘画

AIGC:数字内容创新的新引擎,还有藏着更多你知道的细节

加入高科技仿生人

人工智能 AI AIGC

京东技术专家首推:微服务架构深度解析,GitHub星标120K

程序知音

Java 微服务 springboot java架构 Java进阶

来2023用友BIP技术大会,与北京地铁等领先企业探索数智化转型路径

用友BIP

技术大会 用友iuap 用友技术大会 数智底座 技术底座

想让 ChatGPT 帮忙进行数据分析?你还需要做......

Kyligence

数据分析 指标平台

软件测试/测试开发丨UI自动化测试,PageObject设计模式

测试人

软件测试 自动化测试 测试开发 UI自动化 pageobject

【一行代码秒上云】Serverless六步构建全栈网站

华为云开发者联盟

云计算 华为云 华为云开发者联盟 企业号 4 月 PK 榜

图文介绍 Windows 系统下打包上传 IOS APP 流程

ios 开发

【云享专刊】开源遇上华为云,OCP架构变身“云原生框架”

华为云开发者联盟

开源 云原生 华为云 华为云开发者联盟 企业号 4 月 PK 榜

人人可用的敏捷指标工具!Kyligence Zen 正式发布 GA 版

Kyligence

数据分析 Kyligence Zen 指标平台 大数据管理

理一理事务实现

Zhang

MySQL 事务 数据库·

在高并发场景下保证数据一致性:sync.Map的并发安全性实践

Jack

天天预约|如何使用「代预约」功能?全在这篇文章里!

天天预约

线上预约 预约工具 预约 预约小程序

从一场文学奖评选,看金山文档To B 转型怎么走

B Impact

基于HashData湖仓一体解决方案的探索与实践

酷克数据HashData

MobTech MobLink|裂变拓新,助力运营

MobTech袤博科技

540p秒变1080p!小红书端侧实时超分带你免流量玩嗨短视频

小红书技术REDtech

AI 算法 短视频

小红书自研小程序:电商体验与效果优化的运行时体系设计

小红书技术REDtech

架构 前端

低代码开发,是稳打稳扎还是饮鸩止渴?

引迈信息

前端 低代码 JNPF

AIGC爆火的背后需要掌握的基础原理

飞桨PaddlePaddle

人工智能 AI 百度飞桨 AIGC

一站式指标平台 Kyligence Zen 登陆亚马逊云科技 Marketplace

Kyligence

数据分析 指标中台

跟ChatGPT聊天、需求润色优化,禅道OpenAI 插件发布!

禅道项目管理

项目管理 openai ChatGPT

阿里云 EMAS & 魔笔:3月产品动态

移动研发平台EMAS

阿里云 DevOps 测试 低代码开发 移动端开发

没有研发过程数字化,DevOps就是水中月、雾中花

行云创新

DevOps 研发管理 云原生IDE

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