停止书写回调函数并爱上 ES8
以前, JavaScript 项目会逐渐'失去控制', 其中主要一个原因就是采用传统的回调函数处理异步任务时, 一旦业务逻辑比较复杂, 我们就难免书写一些冗长, 复杂, 嵌套的代码块(回调地狱), 这会严重降低代码的可读性与可维护性. 现在, JavaScript 提供了一种新的语法糖来取代回调函数, 使我们能够编写简明, 可读性高的异步代码.
背景
AJAX
先来回顾一下历史. 在 20 世纪 90 年代后期, Ajax 是异步 JavaScript 的第一个重大突破. 这一技术允许网站在加载 html 后获取并显示最新的数据, 这是一个革命性的想法. 在这之前, 大多数网站会再次下载整个页面来显示更新的内容. 这一技术 (在 jQuery 中以 ajax 的名称流行) 主导了 2000-2010 的 web 开发并且 Ajax 是目前网站用来获取数据的主要技术, 但是 XML 在很大程度上取代了 JSON.
NodeJS
当 NodeJS 在 2009 年首次发布时, 服务器端环境的主要焦点是允许程序优雅地处理并发性. 大多数服务器端语言通过阻塞代码来处理 I/O 操作, 直到操作完成为止. 相反, NodeJS 使用的是事件循环机制, 这样开发人员可以在非阻塞异步操作完成后, 调用回调函数来处理逻辑(类似于 Ajax 的工作方式).
Promises
几年后, NodeJS 和浏览器环境中出现了一种新的标准, 称为 "Promise",Promise 提供了一种强大的, 标准化的方式来组成异步操作. Promise 仍然使用基于回调的格式, 但为链式和组合异步操作提供了一致的语法. 在 2015 年, 由流行的开源库所倡导的 Promise 最终被添加为 JavaScript 的原生特性. Promise 是一个不错的改进, 但它们仍然常常是一些冗长而难以阅读的代码块的原因. 而现在有了一个解决方案. Async/Await 是一种新的语法(从. net 和 C# 中借用), 它允许我们编写 Promise, 但它们看起来像是同步代码, 没有回调, 可以用来简化几乎任何现有的 JS 应用程序. Async/Await 是 JavaScript 语言的新增的特性, 在 ES7 中被正式添加为 JavaScript 的原生特性.
示例
我们将通过一些代码示例来展示 Async/Await 的魅力
注: 运行下面的示例不需要任何库. Async/Await 已经被最新版本的 Chrome,FireFox,Safari,Edge 完全支持, 你可以在你的浏览器控制台里运行例子. Async/Await 需要运行在 NodeJS 7.6 版本及以上, 同时也被 Babel,TypeScript 转译器支持. 所以 Async/Await 可以被用于实际开发之中.
准备
我们会使用一个虚拟的 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)
- })
- }
- }
每个示例依次执行如下三个操作: 获取一个用户的信息, 获取该用户的朋友, 获取该用户的照片. 在最后, 我们会在控制台中打印这些结果.
方法一 --- Nested Promise Callback Functions
使用嵌套的 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 项目开发的人来说, 这个代码块非常熟悉. 非常简单的业务逻辑, 但是代码却是冗长, 深嵌套, 并且以这个结尾.....
- })
- })
- })
- }
在真实的业务场景中, 每个回调函数可能更复杂, 代码块会以一堆充满层次感的})为结尾."回调函数里面嵌套着回调函数嵌套着回调函数", 这就是被传说中的 "回调地狱"("回调地狱" 的诞生不只是因为代码块的混乱, 也源于信任问题.). 更糟糕的是, 我们为了简化, 还没有做错误处理机制, 如果加上了 reject...... 细思极恐
方法二 --- Promise Chain
让我们优雅起来
- 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.prototype.then()和 Promise.prototype.catch()返回 Promise 对象, 这就使得我们可以将这些 promise 连接成一个 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 })
- }
优雅多了, 调用 await 之前我们会一直等待, 直到 promise 被决议并将值赋值给左边的变量. 通过 async/await, 我们可以对异步操作流程进行控制, 就好像它是同步代码.
注: await 必须搭配 async 一起使用, 注意上面的函数, 我们将关键字 async 放在了函数的声明前, 这是必需的. 稍后, 我们会深入讨论这个问题
循环
Async/Await 可以让以前很多复杂的代码变得简明. 举个例子, 如果我们要按序检索每个用户的朋友的朋友列表.
方法一 --- Recursive Promise Loop
下面是使用传统的 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)
- })
- }
我们创建在 promiseLoops 中创建了一个函数用于递归地去获取朋友的朋友列表. 这个函数体现了函数式编程, 但是对于这个简单的任务而言, 这依旧是一个比较复杂的解决方案.
方法二 --- Async/Await For-Loop
让我们尝试一下 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 循环就能解决我们的问题.
并行
一个一个地去获取朋友的朋友的列表看起来有点慢, 为什么不并行处理请求呢? 我们可以用 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.all()会返回一个 promise, 一旦所有的请求完成就会决议.
错误处理
然而, 在异步编程中有一个主要的问题还没解决: 错误处理. 在异步操作中, 我们必须为每个操作编写单独的错误处理回调, 在调用栈的顶部去找出正确的报错位置可能很复杂, 所以我们得在每个回调开始时就去检查是否抛出了错误. 所以, 引入错误处理后的回调函数会比之前复杂度成倍增加, 如果没有主动定位到报错的位置, 这些错误甚至会被 "吞掉". 现在, 我们给之前的例子添上错误处理机制. 为了测试错误处理机制, 我们将在真正获取到用户图片之前使用抽象类里的 api.throwError()方法.
方法一 --- Promise Error Callbacks
让我们看看最坏的情况
- 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 Chain "Catch" Method
我们可以给 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 添加一个错误处理取代了之前给每个回调函数添加错误处理. 但是, 这还是有一点复杂并且我们还是需要使用一个特殊的回调来处理异步错误而不是像对待正常的 JavaScript 错误那样处理它们.
方法三 --- Normal Try/Catch Block
我们可以做得更好
- 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 函数获取数据.
- 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, 这将使得整个异步模块更加明显. 如果我们要获取前面 10 个用户的数据呢?
- 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)
- }
- }
结论
随着 SPA 的兴起和 NodeJS 的广泛应用, 对于 JavaScript 开发人员来说, 优雅地处理并发性比以往任何时候都要重要. Async/Await 缓解了许多因为 bug 引起且已经影响 JavaScript 很多年的控制流问题, 并且使得代码更加优雅. 如今, 主流的浏览器和 NodeJS 都已经支持了这些语法糖, 所以现在是使用 Async/Await 的最好时机.
来源: https://juejin.im/post/5ad1cab8f265da238a30e137