Async/await:让异步编程更简单
介绍
Async/Await 是一个编写异步代码的新方式。之前异步代码的替代方案有回调和 promises。
Async/Await 其实只是一个建立在 promises 之上的一个语法糖。它不能同普通回调或者 node 回调一起使用。
Async/Await 跟 promises 一样,不会阻止代码往下执行。
Async/Await 使得异步代码不论看起来还是行为上都有点像同步代码,这正是它厉害的地方。
语法
function logFetch(url) {return fetch(url).then(response => response.text()).then(text => {console.log(text);return "done";}).catch(err => {console.error('fetch failed', err);});}logFetch();
以下是利用async/await具有相同作用的代码:
async function logFetch(url) {try {const response = await fetch(url);console.log(await response.text());return "done";}catch (err) {console.log('fetch failed', err);}}logFetch();
对比代码可以看到不同之处:
我们的函数前面有一个
async关键字。await关键字只能在同async一起定义的函数内部使用。每一个async函数隐式地返回一个 promise,然后这个 promise resolve 的将会是任意从那个函数的return返回值(在我们的例子中就是字符串"done")。上面那一点表明我们无法在我们代码的顶层使用 await,因为不在
async函数内部。
// 这运行不了// await logFetch()// 这个可以运行logFetch().then((result) => {// do something})
3. await fetch() 意味着在 fetch() 返回的 promise 被 resolve 之后才会调用 console.log 打印值。
为什么 async/await 更好?
1. 简明清晰
sync/await 使得在同一个代码块中同时处理同步和异步错误成为可能,对于 try/catch 有利。在下面使用 promises 的例子中,try/catch 无法处理 JSON.parse 的失败,因为它在 promise 内部。我们需要在 promise 上面调用 .catch,重复处理错误,这比在你的预生产代码里 console.log 要复杂得多。
const logFetch = (url) => {try {fetch(url).then(result => {// this parse may failconst 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 logFetch = async (url) => {try {// this parse may failconst data = JSON.parse(await fetch(url))console.log(data)} catch (err) {console.log(err)}}
3. 条件判断
const logFetch = (url) => {return fetch(url).then(data => {if (data.needsAnotherFetch) {return logAnotherFetch(data).then(moreData => {console.log(moreData);return moreData;})} else {console.log(data);return data;}})}
这看起来相当头痛,很容易在嵌套里看晕(6层嵌套),返回语句只需将最终结果给主 promise。
const logFetch = async (url) => {const data = await fetch(url);if (data.needsAnotherFetch) {const moreData = await logAnotherFetch(data);console.log(moreData);return moreData;} else {console.log(data);return data;}}
4. 中间值(媒介值)
const logFetch = () => {return promise1().then(value1 => {// do somethingreturn promise2(value1).then(value2 => {// do somethingreturn promise3(value1, value2);})})}
如果 promise3 不需要 value1 的话就会容易将 promise 拉平一点。如果想代码简化一点, 可以使用Promise.all 将 value 1 和 value 2 包裹,这样可以避免深层次的嵌套。如下所示:
const logFetch = () => {return promise1().then(value1 => {// do somethingreturn Promise.all([value1, promise2(value1)]);}).then(([value1, value2]) => {// do somethingreturn promise3(value1, value2);})}
这种方式为了可读性牺牲了语义性。因为没有必要让 value1 和 value2 一起放在一个数组里,除了避免 promises 的嵌套。
当使用 async/await 优化时,如下所示:
const logFetch = async () => {const value1 = await promise1();const value2 = await promise2(value1);return promise3(value1, value2);}
5. 错误堆栈
这样一个场景,在一个promise 链中,在后面的某处发生了错误。
const logFetch = () => {return callAPromise().then(() => callAPromise()).then(() => callAPromise()).then(() => callAPromise()).then(() => callAPromise()).then(() => {throw new Error("oops");})}logFetch().catch(err => {console.log(err);// output// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)})
从 promise 链返回的错误堆栈信息无从知道错误到底发生在哪儿。
使用async/await函数
const logFetch = async () => {await callAPromise();await callAPromise();await callAPromise();await callAPromise();await callAPromise();throw new Error("oops");}logFetch().catch(err => {console.log(err);// output// Error: oops at logFetch (index.js:7:9)});
本地开发时可能作用不大,但是对于你排查生产环境的问题时优势就比较明显。在这些状况下,知道错误时发生在 logFetch 比只知道错误在一个又一个 then 后面要好得多。
6. Debugging
你无法在箭头函数的返回表达式中设置断点(无函数体)。
const logFetch = async () => {return callAPromise().then(() => callAPromise()).then(() => callAPromise()).then(() => callAPromise()).then(() => callAPromise())}
2. 当你在 .then 里面打断点时,断点并不会移动到紧邻的下一个 .then 里面去,因为它只会在同步代码中移动断点。
而使用 async/await 时,你就不必使用箭头函数了,而且你可以一步一步的执行 await 调用,因为它们就是普通的同步代码。
const logFetch = async () => {await callAPromise();await callAPromise();await callAPromise();await callAPromise();await callAPromise();}
async/await的缺陷
有一种模式可以缓解这个问题——通过将 Promise 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。
function timeoutPromise(interval) {return new Promise((resolve, reject) => {setTimeout(function(){resolve("done");}, interval);});};
let startTime = Date.now();timeTest().then(() => {let finishTime = Date.now();let timeTaken = finishTime - startTime;alert("Time taken in milliseconds: " + timeTaken);});
第一个示例:
async function timeTest() {await timeoutPromise(3000);await timeoutPromise(3000);await timeoutPromise(3000);}
在这里,我们直接等待所有三个timeoutPromise()调用,使每个调用3秒钟。整个调用完成花费9秒。
第二个示例:
async function timeTest() {const timeoutPromise1 = timeoutPromise(3000);const timeoutPromise2 = timeoutPromise(3000);const timeoutPromise3 = timeoutPromise(3000);await timeoutPromise1;await timeoutPromise2;await timeoutPromise3;}
在这里,我们将三个Promise对象存储在变量中,这样可以同时启动它们关联的进程。
接下来,我们等待他们的结果 - 因为promise都在基本上同时开始处理,promise将同时完成;当您运行第二个示例时,您将看到弹出框报告总运行时间仅超过3秒!
