写点什么

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

评论

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

在手机上运行基于AirTest的自动化脚本

mobileauto

Python 自动化 Airtest

“泡沫”催生行业“繁荣”,切入具身智能赛道正当时!!

机器人头条

机器人 强化学习 大模型 人形机器人 具身智能

GPUStack v0.4:文生图模型、语音模型、推理引擎版本管理、离线支持和部署本地模型

SEAL安全

Stable Diffusion LLM Whisper Speech-to-Text Text-to-Speech

RFID技术在ERP系统中的集成应用

积木链小链

ERP RFID

零代码赋能高等教育数字化转型的实践与思考

明道云

SketchUp Pro 2023:专业级3D建模,轻松实现创意构想

Rose

audirvana mac中文破解版 原生无损音乐播放器

Rose

Cinema 4D R21下载[C4D R21]中文汉化破解版安装方法

理理

GPUStack v0.4:文生图模型、语音模型、推理引擎版本管理、离线支持和部署本地模型

GPUStack

Stable Diffusion LLM Whisper Speech-to-Text Text-to-Speech

OpenAI o1 模型到来后,谈谈提示词工程的未来

Baihai IDP

程序员 AI Prompt LLMs 提示词工程

更轻更省!DataSimba敏捷版直播火热预约中

奇点云

一个明知没啥前途也要开张的市场开张了

明道云

阿里大佬翻遍全网Java面试文章,总结出这份1658页文档,GitHub收获25K+点赞

Summer

Java 编程 程序员 面试 大厂

EndNote X9汉化安装包 及EndNote X9安装教程

Rose

七牛云荣获「2024 鸿蒙生态 SDK 星河奖」

七牛云

最新前端架构设计:中央仓库管理-基于工作空间和git-submodule实现共用和管理

京东科技开发者

信创背景下医院信息化建设的挑战与机遇

明道云

心理行业需要用到堡垒机的几个情形讲解

行云管家

心理 网络安全 等保 堡垒机

从MySQL JOIN 算法角度看如何优化SQL

京东科技开发者

淘宝天猫API接口探索:商品详情与关键字搜索商品列表的实战应用

代码忍者

API 接口 pinduoduo API

普洱市具有资质等保测评机构在哪里?电话多少?

行云管家

网络安全 等保 云南 普洱市

Qt 开发 macOS 应用的技术难点

北京木奇移动技术有限公司

软件外包公司 QT外包开发 QT开发公司

大数据平台Bug Bash大扫除最佳实践

京东科技开发者

WZRY·农活自动化助手

mobileauto

Python 自动化 王者荣耀 手游 Airtest

ARM版CentOS Linux系统镜像文件(苹果M1专用) 及安装教程

Rose

MAMP PRO永久版:本地开发神器!

Rose

创意无限,绘图神器!OmniGraffle Pro,设计新高度

Rose

数字藏品NFT的合约开发

北京木奇移动技术有限公司

软件外包公司 音乐NFT 体育NFT

解锁数据洞察新境界!Tableau Desktop Pro 2020

Rose

想在 Java 八股文面试中脱颖而出?这1000 道互联网大厂 工程师面试题必不可少

Summer

Java 程序员 面试 架构师 编程开发

macOS Developer Beta Access Utility(苹果开发者工具)

Rose

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