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 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 logFetch = async (url) => {
try {
// this parse may fail
const 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 something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2);
})
})
}
如果 promise3 不需要 value1 的话就会容易将 promise 拉平一点。如果想代码简化一点, 可以使用Promise.all 将 value 1 和 value 2 包裹,这样可以避免深层次的嵌套。如下所示:
const logFetch = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)]);
})
.then(([value1, value2]) => {
// do something
return 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秒!