你不知道的async/await 用法

317 阅读5分钟

定义:

async函数是Generator函数的语法糖,其在使用上与Generator函数非常相似,其作用是为了更顺滑的使用Promise。

基本用法:

我们既然说其是Generator函数的语法糖那么首先我们来看一个案例:我们使用Generator函数依次执行两个异步任务与一个同步任务。注意这里虽然是先执行了同步任务,但是异步任务是先被调用的。

                function p1(){
            return new Promise(function(resolve,reject){
                resolve('success1');
            })
        }
        function p2(){
            return new Promise(function(resolve,reject){
                resolve('success2');
            })
        }
        function* fn1(){
            yield p1();
            yield p2();
            yield '这是同步任务';
        }  
        let fn2 = fn1();
        fn2.next().value.then(data => {
            console.log(data);
        })
        fn2.next().value.then(data => {
            console.log(data);
        })
        console.log(fn2.next().value);
                // 这是同步任务 
                // success1
                // success2

接下来我们使用async与await来完成相同的功能:

                function p1(){
            return new Promise(function(resolve,reject){
                resolve('success1');
            })
        }
        function p2(){
            return new Promise(function(resolve,reject){
                resolve('success2');
            })
        };
        async function fn1(){
            let res1 = await p1();
            let res2 = await p2();
            let res3 = await '这是同步任务';
            console.log('res1:'+res1);
            console.log('res2:'+res2);
            console.log('res3:'+res3);
        };
        fn1();
                // res1:success1
                // res2:success2
                // res3:这是同步任务

我们可以发现我们我们不再需要像Generator函数那样不断的调用next方法来执行一个个的异步方法,而且只有上一个await后面的异步函数执行完之后才会执行下一个await后面的异步函数。

对比Generator函数主要有以下几方面不同:

  • 首先其内置执行器,我们不再需要手动调用一个个异步方法,当上一个await执行完之后会自动执行下一个。

  • await命令后面可以跟的值变的更多了,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

  • async函数返回的是一个Promise对象,而Generator函数返回的是一个可迭代对象。

所以async函数也可以看成多个异步操作包装成的Promise对象。

语法:

  • 返回一个Promise对象:async函数无论如何都会返回一个Promsie对象,如果我们给其设置了返回值,那么我们可以在then中拿到该值,即使我们设置的返回值是一个字符串。

    async function f() { return 'hello world'; }

    f().then(v => console.log(v)) // "hello world"

如果async函数内部有错我们可以通过then方法进行捕获。

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了
  • Promsie对象状态的变化:在async函数的内部只有当所有的await命令执行完后我们返回的Promise对象的状态才会变为resolved,除非遇见return或者出现错误。那么如果我们要执行多个异步操作,如果一个异步操作失败我们的async函数便会终止执行,那么我们还想其他异步操作继续执行该怎么办呢?我们可以使用try{}catch(){}来捕获这个错误,确保其他异步操作正常执行。

                function p1(){
            return new Promise(function(resolve,reject){
                // resolve('success1');
                reject('error');
            })
        }
        function p2(){
            return new Promise(function(resolve,reject){
                resolve('success2');
            })
        };
        async function fn1(){
            try{
                let res1 = await p1();
                let res2 = await p2();
                    let res3 = await '这是同步任务';
            }catch(error) {
                console.log(error);
            }
        };
        fn1();
    
  • 执行优化:在实际使用中如果我们的异步操作要一个执行完在执行另一个的话那样有些过于浪费时间了我们可以这样让其同时执行,就是先调用异步操作,然后将异步操作的返回值写在await命令的后面:

    let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;

  • **a****sync 函数可以保留运行堆栈:**就是如果async函数内部的异步操作报错,错误堆栈会包括async函数。而如果是一个普通函数内部的异步操作报错错误堆栈是不包括这个函数的。

    const a = () => { b().then(() => c()); };

上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()c()报错,错误堆栈将不包括a()

现在将这个例子改成async函数。

const a = async () => {
  await b();
  c();
};

上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()c()报错,错误堆栈将包括a()

几种异步操作的特点:

如果我们需要按照顺序来异步加载许多数据,如果我们使用Promise的话,我们需要使用map来遍历我们发送我们的请求,之后使用reduce通过then方法来将所有的Promsie连接起来。

如果我们使用async的话一个for循环便可以解决,代码大大的被简化。总结一句也就是说async适用于依次发送大量请求的情况。

function logInOrder(urls) {
  // 远程读取所有URL
  const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
  });

  // 按次序输出
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text));
  }, Promise.resolve());
}

上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。

这种写法不太直观,可读性比较差。下面是 async 函数实现。

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。

async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。

参考:阮一峰ES6