4. async和await用法以及与promise的关系

921 阅读6分钟

async和await的用法

async

声明异步函数,返回值为一个 Promise 对象,它以类似 同步 的方式来写异步方法

async function fn() {
    console.log('Hello world!');
}

console.log(fn().constructor); // Promise()
// 这里证明其返回值为一个 Promise 对象;

返回值

异步结果是通过 .then() 或者 .catch() 方法来获取并进行进一步处理

// 使用 .then() 的情况
async function fn1() {
    return 'Hello world!';
}

fn1().then(function(res) {
    console.log(res);
});
// Hello world!

// 使用 .catch() 的情况
async function fn3(){
    console.log(aaa); // aaa 依然未定义;
    return 'Hello world!';
}

fn3().then(function(res){
    console.log(res);
}).catch(function(error){
    console.log(error);
});
// ReferenceError: aaa is not defined

await

等待 的意思

var value = await myPromise();

所谓 等待 其实就是指暂停当前 async function 内部语句的执行,等待后面的 myPromise() 处理完返回结果后,继续执行 async function 函数内部的剩余语句;myPromise() 是一个 Promise对象,而自定义的变量 value 则用于获取 Promise 对象返回的 resolve 状态值;

用法

await 必须在 async function,否则会提示语法错误;如果 await 后面跟的是其他值,则直接返回该值。

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function(){
            resolve(2);
        }, 2000);
    });
	console.log(result);
    console.log(3);
    console.log(await 4); // 4 会被直接返回
}
fn();
// 1
// 2 (2 秒后输出)
// 3
// 4

!!!await 会等到后面的 Promise 返回结果 后才会执行 async 函数后面剩下的语句,也就是说如果 Promise 不返回结果(如 resolve 或 reject),后面的代码就不会执行

async function fn() {
    console.log(1);
    await new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log(2);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// 2 (2 秒后输出,并且后面不会继续输出 3)

如果 await 后面的 Promise 返回一个 reject 状态的结果的话,则会被当成错误在后台抛出

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(2);
        }, 2000);
    });
    console.log(3);
}
fn();
// 1
// Uncaught (in promise) 2 (2 秒后输出)

如上,2 秒后会抛出出错误,并且 3 这个数并没有被输出,说明后面的执行也被忽略了

匿名函数

async 也可以用于申明匿名函数用于不同场景,或者嵌套使用 async 函数,如 await async 的形式,只是要在 await 后面使用 async 形式的函数的话,需要这个函数立即执行且有返回值;

let fn = async function() {
    let a = await (async function() {
        console.log(1);
        return 2;
    })();
    console.log(a);

    async function fn2() {
        return 3;
    }
    console.log(await fn2());
}
fn();
// 1
// 2
// 3

await 后面的 Promise 返回的 reject, 也可以被该 async 函数返回的 Promise 对象以 reject 状态获取

async function fn() {
    console.log(1);
    var result = await new Promise(function(resolve, reject) {
        setTimeout(function() {
            reject(2);
        }, 2000);
    });
    console.log(3);
}
fn().catch(function(error) {
    console.log(error);
});
// 1
// 2 (2 秒后输出)

这种情况就不会以错误抛出,直接对异常值进行了处理,并且最后同样没有输出数字 3,即后面的代码依然被忽略了

注意事项

async/await 函数以同步的方式书写异步函数确实方便了不少场景,如定义所讲,函数内部遇到 await 会等到返回结果再继续执行下去,也就是说,非 await 部分仍然会以正常的异步或同步方式执行,例如遇到 setTimeout() 就会放入任务队列等待同步语句执行完后再执行

async function fn() {
    console.log(0);
    
    await new Promise(resolve => {
        setTimeout(() => {
            console.log(1);
            resolve();
        }, 1000);
    });

    setTimeout(() => {
        console.log(2);
    }, 0);

    console.log(3);
}

fn();
// 0
// 1(2 秒后)
// 3
// 2

await 内部

虽然说函数会等待 await 返回结果在继续执行,但是 await 内部的代码也依然按正常的同步和异步执行,例如:

async function fn() {
    console.log(0);
    
    setTimeout(() => {
        console.log(1);
    }, 0);

    await new Promise(resolve => {
        setTimeout(() => {
            console.log(2);
        }, 0);

        console.log(3);

        setTimeout(() => {
            console.log(4);
            resolve();
        }, 1000);

        setTimeout(() => {
            console.log(5);
        }, 0);
    });

    setTimeout(() => {
        console.log(6);
    }, 0);
    console.log(7);
}

fn();
// 0
// 3
// 1
// 2
// 5
// 4(2 秒后)
// 7
// 6

但是假如 await 代码内返回结果的函数(resolve() 或 reject())是在 同步任务 中执行的话,情况就有些不一样了,例如:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    await new Promise(resolve => {
        setTimeout(() => {
            console.log(2);
        }, 0);

        console.log(3);
        resolve();
        console.log(4);

        setTimeout(() => {
            console.log(5);
        }, 0);
    });

    setTimeout(() => {
        console.log(6);
    }, 0);
    console.log(7);
}

fn();
// 0 
// 3
// 4
// 7
// 1
// 2
// 5
// 6

由于同步任务 先于 异步任务执行的机理,在同步任务执行过程中依次输出了 0、3 后,就立即执行了 resolve() 使得 await 得到了返回结果,再往后就继续同步的输出了 4,但是输出 5 的代码是异步任务,与输出 1、2 的代码一并放入任务队列,此时由于 await 返回了结果,所以可以执行 await 以外的代码了,输出 6 是异步任务,于是先输出了同步任务的 7,同步任务都执行完了,最后执行任务队列中的异步任务,按之前进入队列的顺序,就是依次输出 1、2、5、6,所有代码运行结束;

函数嵌套

当 async 函数中嵌套着其他 async 函数时,执行过程可能又有些和预想的不一样

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    (async function() {
        console.log(2);
    
        setTimeout(() => {
            console.log(3);
        }, 0);

        await new Promise(res => setTimeout(res, 1000))

        setTimeout(() => {
            console.log(4);
        }, 1000);

        console.log(5);
    })()

    console.log(6)
}

fn();
// 0
// 2
// 6
// 1
// 3
// 5(1 秒后)
// 4(再等 1 秒后)

也许会疑惑,不是说 async 函数会等到 await 返回结果后再继续执行吗,为何就先输出 6 了?其实不要混淆概念,确实 async 函数内部是这样干的(3 后 1秒输出 5、4),但 async 函数它自身执行时依然是正常的同步任务执行,也就是虽然内部的 async 函数会等待其 await 返回结果才继续执行后面的代码,但外部的 async 函数可不会等待内部的那个 await,会照常执行(你不是我的菜,天涯何处无芳草╮(╯▽╰)╭);

如果确实需要等待这个嵌套的 async 函数执行完再执行剩下的代码,那么前面加个 await 就行了,原理是也是可行的,因为 async 函数就是返回的一个 Promise 函数,代码如下:

async function fn() {
    console.log(0);

    setTimeout(() => {
        console.log(1);
    }, 0);

    await (async function() {
        console.log(2);
    
        setTimeout(() => {
            console.log(3);
        }, 0);

        await new Promise(res => setTimeout(res, 1000))

        setTimeout(() => {
            console.log(4);
        }, 1000);

        console.log(5);
    })()

    console.log(6)
}

fn();
// 0
// 2
// 1
// 3
// 5(1 秒后)
// 6
// 4(再等 1 秒后)

async/await 和 Promise 有什么关系

es2017的新语法,async/await就是generator+promsie的语法糖

await必须在async方法中使用,async返回的也是一个promise对象

如果方法内无await节点:

return 一个字面量则会得到一个{PromiseStatus: resolved}的Promise

throw 一个Error则会得到一个{PromiseStatus: rejected}的Promise。

如果方法内有await节点

async会返回一个{PromiseStatus: pending}的Promise(发生切换,异步等待Promise的执行结果)

Promise的resolve会使得await的代码节点获得相应的返回结果,并继续向下执行。

Promise的reject 会使得await的代码节点自动抛出相应的异常,终止向下继续执行。

forEach 中能否使用 await ?

function test() {
  let arr = [3, 2, 1];
  arr.forEach(async (item) => {
    const res = await fetch(item);
    console.log(res);
  });
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}
test();

执行结果: end 1 2 3

为什么

forEach 只支持同步代码。

image.png forEach 只是简单的执行了下回调函数而已,并不会去处理异步的情况。 并且即使你在 callback 中使用 break 也并不能结束遍历。

怎么解决

for...of循环

因为 for...of 内部处理的机制和 forEach 不同,forEach 是直接调用回调函数,for...of 是通过迭代器的方式去遍历。

async function test() {
  let arr = [3, 2, 1];
  for (let item of arr) {
    let res = await fetch(item);
    console.log(res);
  }
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}
test();

执行结果: Promise {<pending>} 3 2 1 end

for循环

async function test() {
  let arr = [3, 2, 1];
  for (var i = 0; i < arr.length; i++) {
    const res = await fetch(arr[i]);
    console.log(res);
  }
  console.log("end");
}

function fetch(x) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(x);
    }, 500 * x);
  });
}

test();

执行结果: Promise {<pending>} 3 2 1 end