异步函数

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按顺序收到了每个期约的值。