「译」7 个用 Async/Await 取代 Promise 的理由(教程)

作者:Mostafa Gaafar
原文链接:7 Reasons Why JavaScript Async/Await Is Better Than Plain Promises (Tutorial)

Async/Await 由 NodeJS 7.6 版本引入,现在已经被所有现代浏览器所支持。我认为这是 JS 自 2017 年以来最棒的更新。如果你不信,这里有一堆带着示例的理由告诉你为什么你应该尽快接受它并且永不再回头。

Async/Await 概述

对那些从未听说过这个话题的,这里有一个简介:

  • Asyne/Await 是一种新的异步代码写法。以前用于写异步代码的是 callback 和 Promise。
  • Async/Await 实际上是基于 Promise 的语法糖。无法用在只有 callback 或 node callba 的情形中。
  • Async/Await 跟 Promise 一样是非阻塞的。
  • Async/Await 使得异步代码的写法和行为都有点类似同步代码。这正是它强大的地方。

语法

假设有一个 getJSON 函数,它返回一个 Promise,然后这个 Promise 会解析出几个 JSON 对象。我们要调用这个函数,打印出 JSON 并返回一个 “done” 字符串。

这是用 Promise 方式实现:

1
2
3
4
5
6
7
8
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})

makeRequest()

然后这是用 Async/Await 方式实现:

1
2
3
4
5
6
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}

makeRequest()

这里有几点不同之处:

  1. 函数前面有 async 关键字。await 关键字只能用在由 async 关键字定义的函数里。async 函数会隐式的返回一个 Promise,这个 Promise 的解析值是函数的返回值(在这个例子中是 "done" 字符串)。

  2. 上面提到的这点也暗示了我们不能将 await 用在我们代码的最外层,因为它必须被放置在 async 函数内部。

    1
    2
    3
    4
    5
    6
    7
    // this will not work in top level
    // await makeRequest()

    // this will work
    makeRequest().then((result) => {
    // do something
    })
  3. await getJSON() 意思是 console.log 将会等待 getJSON 的 Promise 解析出结果,然后才把结果打印出来。

好在那里呢?

  1. 简洁干净
    看我们节省了多少代码!即使在我们随手编的示例代码中,也能明显的看到省了不少代码。我们无需再写 .then 然后用一个匿名函数来处理返回值,也不需要定义一个 data 变量来表示返回值。另外我们还避免了代码嵌套。接下来的例子中我们将更明显的看到这一个个的优点将累积成巨大的优势。

  2. 错误处理
    Async/Await 让我们可以用惯用的 try/catch 结构来处理同步和异步错误。下面的例子中有一个 Promise,try/catch 无法捕获 JSON.parse 产生的错误,因为这个错误是在 Promise 内部产生的。我们需要在 Promise 上调用 .catch 方法并复制一份错误处理的代码到这里,在你的生产代码中,这样写应该会比 console.log 更稳妥一些。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const makeRequest = () => {
    try {
    getJSON()
    .then(result => {
    // 这个解析将会失败
    const data = JSON.parse(result)
    console.log(data)
    })
    // 取消此处注释以处理异步错误
    // .catch((err) => {
    // console.log(err)
    // })
    } catch (err) {
    console.log(err)
    }
    }

    现在我们用 async/await 来写同样的代码。现在 catch 代码将能够处理解析错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const makeRequest = async () => {
    try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
    } catch (err) {
    console.log(err)
    }
    }
  3. 条件语句
    假设下面的代码将会获取一些数据,并根据数据内容决定是要返回数据还是基于获取的内容进行进一步操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const makeRequest = () => {
    return getJSON()
    .then(data => {
    if (data.needsAnotherRequest) {
    return makeAnotherRequest(data)
    .then(moreData => {
    console.log(moreData)
    return moreData
    })
    } else {
    console.log(data)
    return data
    }
    })
    }

    光是看这些代码就让人头疼。而且这么多的嵌套(6 层)、括号、以及为了把最终结果传递到主 Promise 的 return 语句很容易让人晕头转向。

    当我们用 async/await 重写这个例子后,代码则变得非常易读。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const makeRequest = async () => {
    const data = await getJSON()
    if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
    } else {
    console.log(data)
    return data
    }
    }
  4. 中间值
    你可能会遇到这样的情形:调用 Promise1 然后用它的返回值调用 Promise2,然后再用它们两个的返回值来调用 Promise3。你的代码可能会像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const makeRequest = () => {
    return promise1()
    .then(value1 => {
    // do something
    return promise2(value1)
    .then(value2 => {
    // do something
    return promise3(value1, value2)
    })
    })
    }

    如果 promise3 不需要 value1 那么就可以将 Promise 嵌套弄的扁平一些。要是你无法接受这种做法,那么你可以把 value1 和 value2 都包在一个 Promise.all 中来避免深层的嵌套,像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const makeRequest = () => {
    return promise1()
    .then(value1 => {
    // do something
    return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {
    // do something
    return promise3(value1, value2)
    })
    }

    这种方案为了可读性牺牲了部分语义。将 value1value2 放在同一个数组中没有任何意义,只是为了避免 Promise 的嵌套。

    用 async/await 实现同样的逻辑异乎寻常的简单和直观。它让你质疑自己那些花在拼命优化 Promise 代码上的时间本可以做多少美妙的事情。

    1
    2
    3
    4
    5
    const makeRequest = async () => {
    const value1 = await promise1()
    const value2 = await promise2(value1)
    return promise3(value1, value2)
    }
  5. 错误堆栈
    设想有一段链式调用多个 Promise 的代码,在链的某一部分,抛出了一个错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const makeRequest = () => {
    return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
    throw new Error("oops");
    })
    }

    makeRequest()
    .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
    })

    Promise 链返回的错误的堆栈没有给出错误产生位置的信息。更糟糕的是,它还有误导性;它包含的唯一一个函数名 callAPromise 在这里完全是无辜的(不过文件名和行号还是有用的)。

    相反,async/await 给出的错误堆栈指出了真正包含错误的函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const makeRequest = async () => {
    await callAPromise()
    await callAPromise()
    await callAPromise()
    await callAPromise()
    await callAPromise()
    throw new Error("oops");
    }

    makeRequest()
    .catch(err => {
    console.log(err);
    // output
    // Error: oops at makeRequest (index.js:7:9)
    })

    当你开着编辑器在本地写代码时,这可能影响不大,不过当你试图从生产服务器分析错误日志时用处就大了。这时候,知道错误产生在 makeRequest 要好过知道错误发生在一个 then 后的 then 后的 then

  6. 调试
    用 async/await 的一个杀手级优势是调试起来更简单。由于以下两个原因,调试 Promise 一直是个痛点:

    1. 返回值是表达式(没有函数体)的箭头函数无法设置断点。

      有本事在这儿加个断点试试

    2. 如果你在某个 .then 代码块内部设置断点然后使用步进调试,调试器不会移到下一个 .then 中,因为它只能”步进“同步代码。

      使用 async/await 时不需要太依赖箭头函数,并且你可以像调试同步代码那样步进 await 调用。

  7. 你可以 await 一切
    最后的要点,await 在同步和异步代码中都可以使用。例如你可以写 await 5, 相当于 `Promise.resolve(5)。乍看起来可能没啥用,但是在你写工具或库函数时,用于处理不确定输入是同步还是异步的情况时是非常有用的。

    假如你想记录你应用里某个 API 执行的时间,并希望这个记录函数是通用的。用 Promise 实现如下:

    1
    2
    3
    4
    5
    6
    7
    const recordTime = (makeRequest) => {
    const timeStart = Date.now();
    makeRequest().then(() => { // throws error for sync functions (.then is not a function)
    const timeEnd = Date.now();
    console.log('time take:', timeEnd - timeStart);
    })
    }

    我们知道这里所有的 API 调用都返回 Promise,但是如果你用这个函数去记录一个同步函数的执行时间呢?它将会抛出一个错误因为同步函数不返回 Promise。一个很有效的解决方法是把函数包裹在 Promise.resolve() 中。

    如果使用 async/await,你就无需担心上述情况,因为 await 让你安全的处理任何值,无论是否是 Promise。

    1
    2
    3
    4
    5
    6
    const recordTime = async (makeRequest) => {
    const timeStart = Date.now();
    await makeRequest(); // works for any sync or async function
    const timeEnd = Date.now();
    console.log('time take:', timeEnd - timeStart);
    }

结论

Async/Await 是近年来 JavaScript 添加的革命性的特性之一。它让你认识到 Promise 语法是多么混乱,并提供了一个更直观的替代方式。

疑虑

在使用 async/await 时你可能会有些疑虑,因为它让异步代码的特征不那么明显了:以前我们一看到 .then 回调就知道是异步代码,现在可能需要花几周时间来适应新的写法,不过 C# 中已经引入这个特性好几年了,了解它的人都知道这短暂的不适应是值得的。

Follow me on twitter @imgaafar

This article was originally published on Hackernoon

「译」如何制作 SVG 线条动画 「译」理解 JavaScript 中的 Promise
广告: