Async/Await 在 NodeJS 7.6 中引入,目前所有的现代浏览器中均能支持使用。相信自 2017 年以来,它已成为 JS 最大的补充。如果你不相信的话,那么有很多理由能向你说明为什么应该立即使用它而不是停滞不前。
# Async/Await
Async/Await
简单介绍:
- Async/Await 是一种编写异步代码的新方法。异步代码之前的替代方法是 callback 和 promises。
- Async/Await 实际上是在 promises 之上构建的语法糖。它不能与普通回调和 node 回调一起使用。
- Async/Await 和 promises 一样是非阻塞的。
- Async/Await 使得异步代码的外观看起来更像同步代码。
# 语法
假设一个 getJSON 函数返回一个 promise,并且这个 promise resolve 一个 JSON 对象。我们只想调用 getJSON 并且打印出 JSON,然后返回 done
。
下面是使用 promises 实现方式:
const makeRequest = () => | |
getJSON() | |
.then(data => { | |
console.log(data) | |
return "done" | |
}) | |
makeRequest() |
下面,让我们看看使用 async/await
是如何实现的:
const makeRequest = async () => { | |
console.log(await getJSON()) | |
return "done" | |
} | |
makeRequest() |
上门两种写法的不同之处:
- 我们的函数之前有关键字
async
,关键字await
只能在使用async
定义的函数内部使用。任何async
函数都会隐式的返回 promise,并且 promiseresolve
的值是我们从函数中返回的值(上述例子里是done
) - 上面的例子说明我们不能在代码最顶层直接使用
await
,因为它不在异步函数中。
// this will not work in top level | |
// await makeRequest() | |
// this will work | |
makeRequest().then((result) => { | |
// do something | |
}) |
await getJSON()
意味着console.log
将会在getJSON()
的 promise resolves 后调用并打印getJOSN
返回值。
# 为什么 Async/Await 更好
# 1.Concise and clean
相比于 promise 实例中的代码,使用 Async/Await 中我们节省了大量代码。我们不需要写 .then
,然后创建一个匿名函数来处理响应,也不必为不需要使用的数据命名。我们还可以避免代码嵌套代码。这些优点累加起来在实例中更加明显。
# 2.Error handling
Async/Await 使我们使用相同的 construct 去处理同步和异步的错误成为可能,在下面的 promises 实例中,如果 JSON.parse
如果报错 try/catch 不会处理,因为它是发生在 promises 内部的。我们需要在 promise 上调用 .catch
方法来处理错误。
const makeRequest = () => { | |
try { | |
getJSON() | |
.then(result => { | |
// this parse may fail | |
const data = JSON.parse(result) | |
console.log(data) | |
}) | |
// uncomment this block to handle asynchronous errors | |
// .catch((err) => { | |
// console.log(err) | |
// }) | |
} catch (err) { | |
console.log(err) | |
} | |
} |
现在看看使用 async/await 去做相同的事, catch
现在回处理错误了:
const makeRequest = async () => { | |
try { | |
// this parse may fail | |
const data = JSON.parse(await getJSON()) | |
console.log(data) | |
} catch (err) { | |
console.log(err) | |
} | |
} |
# 3.Conditionals
想象一下下面这样的代码,他获取一些数据并根据数据中的某些值决定是否返回或者还是获取等多信息。
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 | |
} | |
}) | |
} |
只是看上面的代码是否会让你头疼~,很容易迷失在代码的嵌套、括号和 return 语句中,而这些只是需要将最后结果返回给主 promise 即可。
当使用 async/await 重写时,代码会变得更加可读。
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.Intermediate values
你可能发现你处于这样一种情况:调用一个 promise1,然后使用其返回内容调用 promise2,然后使用两个 promise 的结果调用 promise3. 代码可能如下:
const makeRequest = () => { | |
return promise1() | |
.then(value1 => { | |
// do something | |
return promise2(value1) | |
.then(value2 => { | |
// do something | |
return promise3(value1, value2) | |
}) | |
}) | |
} |
如果 promise3 不需要使用 value1,那么将很容易去简化我们的嵌套代码,若果你是那种无法忍受这种写法的人的话,你可以将 value1 和 value2 包装起来传入 Promise.all
中,就像下面这样:
const makeRequest = () => { | |
return promise1() | |
.then(value1 => { | |
// do something | |
return Promise.all([value1, promise2(value1)]) | |
}) | |
.then(([value1, value2]) => { | |
// do something | |
return promise3(value1, value2) | |
}) | |
} |
为了易于阅读,上述方法牺牲了语义。除了避免嵌套 promise,没有理由将 value1 和 value2 放入一个数组中。
使用 acync/await,相同的逻辑将变得非常简单且直观。
const makeRequest = async () => { | |
const value1 = await promise1() | |
const value2 = await promise2(value1) | |
return promise3(value1, value2) | |
} |
# 5.Error stacks
想象一下一段代码,它在一个链中调用多个回调,而在链的某处,会抛出一个错误。
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 |
从 promise 链中返回的错误堆栈,我们无法定位错误发生的位置。然而,如果错误信息的堆栈来自 async/await 的话,它将直接指向发生错误的函数:
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 更好。
# 6.Debugging
使用 async/await 最大的优势是调试起来非常方便。调试 promise 一直很痛苦,原因如下:
- 不能在返回表达式的箭头函数中设置断点(无正文)
const makeRequest = () => { | |
return callAPromise() | |
.then(() => callAPromise()) | |
.then(() => callAPromise()) | |
.then(() => callAPromise()) | |
.then(() => callAPromise()) | |
} |
- 如果在 .then 中设置断点并使用诸如 step-over 的调试快捷方式,调试器将不会移动到以一个 .then。
使用 async/await,我们不需要那么多箭头函数,并且可以完全像正常的同步函数那样逐步执行 await 调用。
const makeRequest = async () => { | |
await callAPromise() | |
await callAPromise() | |
await callAPromise() | |
await callAPromise() | |
await callAPromise() | |
} |
# 7.You can await anything
await 可以用于同步和异步表达式。例如可以编写 await 5
,它等于 Promise.resolve(5)
。这看起来似乎没什么用,但是在编写不知道输入是同步还是异步的库或实用函数时,这其实是一个很大的优势。
假设你要记录应用中执行一些 API 所需要的时间,然后你决定为此创建一个通用函数,下面是 promise 写法:
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,但是如果使用相同的函数记录同步函数中花费的时间会怎样?因为 sync 函数不会返回 promise,所有将会引发错误,因为同步函数中不会返回 promise,所以会报错。避免这种情况发生通常是将 makeRequest 包装在 promise.resolve 中。
如果使用 async/await,则不必担心这些情况发生。
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 最具革命性的功能之一。它是我们意识到了代码的混乱,并提供了直观的改进方法。