javascript-async/await

loading 2022年11月18日 176次浏览

1. 含义

async/await在es2017被引入,作为Generator函数的语法糖。

最主要的变化,就是将Generator的*号替换成了async,将yield替换成了await。

        const asyncFn = async () =>{
            const a = await 1;
            const b = await 2;
            console.log(a , b)
        }
        asyncFn() //1 2


        const genFn = function* (){
            yield 1;
            yield 2;
        }
        const it = genFn();
        console.log(it.next())
        console.log(it.next())

2.优势

async函数对于Generator函数的改进主要体现在以下方面:
(1)内置执行器
Generator的执行依赖于执行器(因此才有了co模块)。而async函数自带执行器,也就是说async的执行和普通函数一样,直接写就可以了。

        asyncFn();

(2)更好的语义
async函数的语义更加清晰易懂:async表示函数里有异步操作,await用于等待一个Promise兑现并获取它兑现之后的值。

(3)更广的适用性
co模块中,yield命令后面只能是thunk函数或者Promise对象,而await后面可以是原始类型,Promise对象和thenable对象等等。(原始类型的值会被自动转成立即resolved的Promise 对象)

(4)返回值更好处理
Generator返回迭代器,而async函数返回Promise对象,因此可以方便地利用then方法指定下一步的操作。

3.用法

async函数返回一个Promise对象,可以用then添加回调函数。

当async函数执行时,一旦遇到await函数就进入等待,等待await后的Promise对象(基础类型会被resolve成Promise)兑现后再继续执行。

例如这个例子,执行asyncFn时遇到await表达式,就要先暂停执行,等待2s到后面的Promise实例兑现后await表达式的值变成123,再继续执行asyncFn打印出来。

        const promiseFn = val => {
            return new Promise((resolve , reject) =>{
                setTimeout(()=>{
                    resolve(val);
                } , 2000)
            })
        }

        const asyncFn = async () =>{
            const a = await promiseFn(123);
            console.log(a);
        }

        asyncFn()//两秒后打印123

放在then方法里处理也是同理

注意,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数

        const asyncFn = async () =>{
            const a = await promiseFn(123);
            return a;
        }

        asyncFn().then(val => console.log(val))

捕获错误也是同理,都是基础的Promise操作

        const asyncFn = async () =>{
            throw new Error('sorry')
        }

        asyncFn().then(val => console.log(val) , err => console.log(err))

await后面也可以是thenable对象(定义了then方法的对象)

        class Sleep{
            constructor(time){
                this.time = time;
            }
            then(resolve , reject){
                const startTime = Date.now();
                setTimeout(()=>{
                    resolve(Date.now() - startTime);
                } , this.time)
            }
        }

        const asyncFn = async () =>{
            const sleepTime = await new Sleep(1000);
            console.log(sleepTime);
        }
        asyncFn() //1000左右 会有几微秒的误差

也可以利用async/await实现sleep功能

        const sleep = (delay) =>{
            return new Promise((resolve , reject) =>{
                setTimeout(()=>{
                    resolve();
                } , delay)
            })
        }

        const fn = async() =>{
            for(let i = 1 ; i<=5 ; i++){
                await sleep(1000);
                console.log(i);
            }
        }

        fn(); // 间隔1s输出1 2 3 4 5

还可以实现能够cancel的sleep函数

function cancellableSleep(millis) {
    let timeoutId = null;
    const promise = new Promise((resolve) => {
        timeoutId = setTimeout(resolve, millis);
    });
    const cancelFn = () => {
        clearTimeout(timeoutId);
    };
    return { promise, cancelFn };
}

// 使用示例
const { promise, cancelFn } = cancellableSleep(1000);
promise.then(() => console.log('Finished'));
setTimeout(cancelFn, 500);  // 能够取消
setTimeout(cancelFn, 1500);  // 不能取消

如果一个await语句后面的Promise对象变成reject状态,那么async函数就会被中断

        async function f() {
            await Promise.reject('出错了');
            await Promise.resolve('hello world'); // 不会执行
        }

可以通过try-catch代码块解决

        const fn = async () =>{
            try{
                await Promise.reject('sorry');
            }catch(e){}

            return await Promise.resolve(123)
        }
        fn()
        .then(val => console.log(val)) //123

或者直接在await后面catch

        async function f() {
            await Promise.reject('出错了')
                .catch(e => console.log(e));
            return await Promise.resolve(123);
        }

        f()
        .then(v => console.log(v))
        // 出错了
        // 123

可以通过try-catch实现多次尝试fetch某网址,成功了就退出。

比如fetch自己的博客,一次就成功了直接退出,打印0。

        async function test() {
            let i;
            for (i = 0; i < 3; ++i) {
                try {
                    await fetch('http://120.25.100.22:12347/');
                    break;
                } catch (err) { }
            }
            console.log(i); // 3
        }

        test(); //0

4.使用注意点

(1)await后面的Promise对象如果可能是rejected状态,最好放在try-catch中(示例在上面)

(2)多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

比如这里getFoo()和getBar()是两个不相关的异步操作

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

如果写成顺序继发关系会比较耗时,因为只有等getFoo()这个异步操作完成后才轮到getBar(),最好让他们同时触发。

        // 写法一
        let [foo, bar] = await Promise.all([getFoo(), getBar()]);

        // 写法二
        let fooPromise = getFoo();
        let barPromise = getBar();
        let foo = await fooPromise;
        let bar = await barPromise;

(3)注意await不要放到普通函数/forEach中使用
forEach里的函数参数是个普通函数

        async function dbFuc(db) {
            let docs = [{}, {}, {}];

            // 报错
            docs.forEach(function (doc) {
                await db.post(doc);
            });

但是就算将forEach方法改成async函数也会出错,因为async/await语句在forEach中不能正常运行。

原因见这里

        let sum = 0
        const arr = [1, 2, 3]

        async function sumFn(a, b) {
            return a + b
        }

        function main(array) {
            array.forEach(async item => {
                sum = await sumFn(sum, item)
            })
            console.log(sum) // 0, Why?
        }
        main(arr)

因此最好用for...of来代替

        let sum = 0
        const arr = [1, 2, 3]

        async function sumFn(a, b) {
            return a + b
        }

        // await 要放在 async 函数中
        async function main(array) {
            for (let item of array) {
                sum = await sumFn(sum, item)
            }
            console.log(sum) // 6
        }
        main(arr)

5.和Promise实例对比

实例一
Promise

        const foo = (name) =>{
            return new Promise((resolve , reject) =>{
                console.log(name , 1);
                resolve(console.log(name , 2))
            })
            .then(() => console.log(name , 3))
        }
        foo('apple');
        foo('banana');

        //apple 1
        //apple 2
        //banana 1
        //banana 2
        //apple 3
        //banana 3

async/await

        const foo = async (name) =>{
            console.log(name , 1);
            await console.log(name , 2);
            console.log(name , 3);
        }
        foo('apple');
        foo('banana');
	//结果同上

实例二
Promise

        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());
        }

async/await继发版本

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

async/await并发优化版本

        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);
            }
        }

6.顶层await

在模块的顶层,可以单独使用await异步函数的外面)。也就是说一个模块如果包含用了await的子模块,该模块就会等待该子模块,这一过程并不会阻塞其它子模块。

下面是一个在export表达式中使用了Fetch API的例子。任何文件只要导入这个模块,后面的代码就会等待,直到fetch完成。

// fetch request
const colors = fetch("../data/colors.json").then((response) => response.json());

export default await colors;

7. for await of

使用for await of遍历时,会等待前一个Promise对象的状态改变后,再遍历到下一个成员。

        function Gen(time){
            return new Promise((resolve , reject) =>{
                setTimeout(() =>{
                    resolve(time)
                } , time)
            })
        }
        async function test(){
            let arr = [Gen(1000) , Gen(2000) , Gen(3000)];
            for await(let item of arr){
                console.log(Date.now() , item);
            }
        }
        test();
        //1669562023046 1000
        //1669562024049 2000
        //1669562025051 3000