异步函数
async
此关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。
用法如下:
async function foo(){}
let bar = async function(){};
let baz = async ()=>{};
class Qux{
async foo(){}
}
使用async
关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。如下面例子所示,foo()
函数仍然会在后面的指令之前被求值。
async function foo(){
console.log(1)
}
foo();
console.log(2)
//1
//2
异步函数如果使用了return
关键字返回了值(如果没有return
则会返回undefined
),这个值会被Promise.resolve()
包装成一个期约对象。异步函数始终返回期约对象。
async function foo(){
console.log(1)
return 3
}
foo().then(console.log);
console.log(2)
//1
//2
//3
与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约:
async function foo(){
console.log(1)
throw 3
}
foo().catch(console.log);
不过拒绝期约的错误不会被异步函数捕获
async function foo(){
console.log(1)
Promise.reject(3);
}
foo().catch(console.log);
console.log(2);
await
因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用await
关键字可以暂停异步函数代码的执行,等待期约解决。
let p= new Promise((resolve, reject) => setTimeout(resolve,1000,3));
p.then((x)=>console.log(x))
//使用async和await可以写成这样
async function foo(){
let p= new Promise((resolve, reject) => setTimeout(resolve,1000,3));
let x= await p;
console.log(x);
}
注意,await
关键字会暂停执行异步函数后面的代码,让出JS
运行时的执行县城。这个行为与生成器函数中的yield
关键字是一样的。await
关键字同样时尝试“解包”对象的值,然后讲这个值传递给表达式,再异步恢复异步函数的执行。
async function foo(){
console.log(await Promise.resolve('foo'))
}
foo();
async function bar(){
return await Promise.resolve('bar')
}
bar().then(console.log);
async function baz(){
await new Promise.resolve((resolve, reject) => setTimeout(resolve,1000));
console.log('baz')
}
baz()
单独的Promise.reject()
不会被异步函数捕获,而会抛出未捕获错误。不过,对拒绝的期约使用await
则会释放错误值。
//释放错误值
async function foo() {
console.log(1);
await Promise.reject(3);
console.log(4)//这行代码不会执行
}
//给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
//1
//2
//3
await的限制
await
关键字必须在异步函数中使用,不能在顶级上下文如<script>
标签或模块中使用。不过,定义并立即调用异步函数是没问题的。
async function foo() {
console.log(await Promise.resolve(3));
}
foo();
//3
(async function(){
console.log(await Promise.resolve(3));
})();
//3
异步函数的特质不会扩展到嵌套函数。因此,await
关键字也只能直接出现在异步函数的定义中。在同步函数内部使用await
会抛出SyntaxError
停止和恢复执行
JS
运行时在碰到await
关键字时,会记录在哪里暂停执行。等到await
右边的值可用了,JS
运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
因此,即使await
后面跟着一个立即可用的值,函数的其余部分也会被异步求值。
async function foo(){
console.log(2);
await null;
console.log(4);
}
console.log(1);
foo();
console.log(3);
//1
//2
//3
//4
下面例子虽然看起来很反直觉,但它演示了真正的执行顺序。
async function foo(){
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar(){
console.log(4);
console.log(await 6);
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
//1
//2
//3
//4
//5
//6
//7
//8
//9
异步函数策略
实现sleep()
可以在程序中模仿java
加入非阻塞的暂停。
//实现sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function foo(){
const t0 = Date.now();
await sleep(1000);
console.log(Date.now() -t0);
}
foo();
利用平行执行
下面这个例子,顺序等待了5个随机的超时。
//利用平行执行
async function randomDelay(id){
const delay =Math.random()*1000;
return new Promise(resolve =>setTimeout(()=>{
console.log(`${id}finished`)
resolve();
},delay));
}
async function foo(){
const t0 = Date.now();
for(let i = 0; i <5;i++){
await randomDelay(i);
}
console.log(Date.now() -t0);
}
foo();
//0 finished
//1 finished
//2 finished
//3 finished
//4 finished
//2447
就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。
如果顺序不是必须保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。
async function randomDelay(id){
const delay =Math.random()*1000;
return new Promise(resolve =>setTimeout(()=>{
console.log(`${id}finished`)
resolve();
},delay));
}
async function foo(){
const t0 = Date.now();
const promises = Array(5).fill(null).map((_,i) =>randomDelay(i));
for(const p of promises){
await p;
}
console.log(Date.now() -t0);
}
foo();
//2finished
//0finished
//4finished
//3finished
//1finished
//461
注意,虽然期约没有按照顺序执行,但await
按顺序收到了每个期约的值。
- 本文链接:https://archer-lan.github.io/2023/11/20/JS-%E5%BC%82%E6%AD%A5%E5%87%BD%E6%95%B0/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。