首页 > 其他分享 >JS异步中async、await讲解

JS异步中async、await讲解

时间:2024-07-27 19:28:40浏览次数:7  
标签:resolve console log await JS Promise async

目录

1 async、await

关于promise、async/await的使用相信很多小伙伴都比较熟悉了,但是提到事件循环机制输出结果类似的题目,敢说都会?

1.1 微任务队列&宏任务队列

JavaScript 中,事件循环(Event Loop)机制负责协调代码的执行顺序。为了理解 JavaScript 的执行顺序和异步行为,了解微任务队列(Microtask Queue)和宏任务队列(Macrotask Queue,或称为 Task Queue)是非常重要的。

  • 事件循环JavaScript 的一种机制,它处理所有异步操作,并在合适的时间将它们放入执行队列中。
  • 宏任务队列(Macrotask Queue
    宏任务队列中的任务通常会在事件循环的每一轮中执行一次。每次事件循环迭代(tick)都会从宏任务队列中取出一个任务执行。
    宏任务队列包含了所有的宏任务(或称为任务),这些任务包括:
    • setTimeoutsetInterval:定时器任务。
    • I/O 操作:文件读写、网络请求等。
    • setImmediateNode.js 环境下的任务。
    • UI 渲染:浏览器的 UI 任务。
  • 微任务队列(Microtask Queue
    微任务会在当前宏任务执行完毕后、下一次事件循环之前执行。换句话说,微任务队列中的任务会在宏任务队列中的任务之前被执行
    微任务队列包含了所有的微任务,主要包括:
    • Promisethencatch 回调:Promise 对象的异步回调。
    • MutationObserver:用于监听 DOM 变动的 API。
    • queueMicrotask:手动添加微任务。
  • 执行顺序
    执行同步代码:事件循环从栈中取出同步代码执行。
    处理微任务队列:执行所有微任务队列中的任务。微任务会在当前宏任务执行完毕后立刻执行,直到微任务队列为空。
    执行宏任务队列:从宏任务队列中取出一个任务执行。
    渲染:如果有必要,浏览器会进行 UI 更新。
console.log('Start');

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

Promise.resolve().then(() => {
    console.log('Promise 1');
}).then(() => {
    console.log('Promise 2');
});

console.log('End');

最终输出结果是:
Start
End
Promise 1
Promise 2
Timeout

同步代码执行:

  • console.log('Start') 输出 Start。
  • setTimeout 设置一个定时器,任务被放入宏任务队列。
  • Promise.resolve().then() 的回调被放入微任务队列。
  • console.log('End') 输出 End。
  • 处理微任务队列:
    • 执行微任务队列中的回调:Promise 1 和 Promise 2。
  • 处理宏任务队列:
    • 执行宏任务队列中的回调:Timeout。

1.2 问题引入

例子一:

async function async1 () {
    await new Promise((resolve, reject) => {
        resolve()
    })
    console.log('A')
}

async1()

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

// 最终结果: B A C D

例子二:

async function async1 () {
    await async2()
    console.log('A')
}

async function async2 () {
    return new Promise((resolve, reject) => {
        resolve()
    })
}

async1()

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

// 最终结果: B C D A

1.3 async 函数返回值

在讨论 await 之前,先聊一下 async 函数处理返回值的问题,它会像 Promise.prototype.then 一样,会对返回值的类型进行辨识。
根据返回值的类型,引起 js引擎 对返回值处理方式的不同

async 函数在抛出返回值时,会根据返回值类型开启不同数目的微任务

  • return结果值:非thenable、非promise不等待
  • return结果值:thenable(等待 1个then的时间)
  • return结果值:promise(等待 2个then的时间)

1.3.1 示例

示例1:不等待

async function testA () {
    return 1;
}
testA().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));

// (不等待)最终结果: 1 2 3

示例2:一个then

async function testB () {
    return {
        then (cb) {
            cb();
        }
    };
}

testB().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));

// (等待一个then)最终结果: 2 1 3

示例3:两个then

async function testC () {
    return new Promise((resolve, reject) => {
        resolve()
    })
}

testC().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3));
    
// (等待两个then)最终结果: 2 3 1

示例4:两个then

async function testC () {
    return new Promise((resolve, reject) => {
        resolve()
    })
} 

testC().then(() => console.log(1));
Promise.resolve()
    .then(() => console.log(2))
    .then(() => console.log(3))
    .then(() => console.log(4))

// (等待两个then)最终结果: 2 3 1 4

1.3.2 面试示例

稍安勿躁,来试试一个经典面试题

async function async1 () {
    console.log('1')
    await async2()
    console.log('AAA')
}

async function async2 () {
    console.log('3')
    return new Promise((resolve, reject) => {
        resolve()
        console.log('4')
    })
}

console.log('5')

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

async1()

new Promise((resolve) => {
    console.log('7')
    resolve()
}).then(() => {
    console.log('8')
}).then(() => {
    console.log('9')
}).then(() => {
    console.log('10')
})
console.log('11')

最终结果: 5 1 3 4 7 11 8 9 AAA 10 6

步骤讲解:

  • 先执行同步代码,输出5
  • 执行 setTimeout,是放入宏任务异步队列中
  • 接着执行 async1函数,该函数是异步的,但会立即开始执行其内部的同步代码,输出1
  • 执行async2函数,输出3
  • 创建一个Promise,并立即调用 resolve()。但重要的是要注意,console.log('4')是在resolve()调用之后立即执行的,这意味着'4'会在Promise解决之后但在任何then回调之前打印,即:Promise 构造器中代码属于同步代码,输出4
  • async2函数的返回值是Promise,等待2个then后放行,所以AAA暂时无法输出
  • async1函数暂时结束,继续往下走,输出7,
    创建一个新的 Promise,并立即打印'7'然后解决它。这个Promisethen回调将被添加到微任务队列中
  • 同步代码,输出11
  • 执行第一个then,输出8
  • 执行第二个then,输出9
  • 终于等到了两个then执行完毕,执行async1函数里面剩下的,输出AAA
  • 再执行最后一个微任务then,输出10
  • 执行最后的宏任务setTimeout,输出6

1.4 await 右值类型区别

1.4.1 非 thenable

await 后面接非 thenable 类型,会立即向微任务队列添加一个微任务 then,但不需等待

async function test () {
    console.log(1);
    await 1;
    console.log(2);
}
test();
console.log(3);
// 最终结果: 1 3 2
function func () {
    console.log(2);
}
async function test () {
    console.log(1);
    await func();
    console.log(3);
}

test();
console.log(4);

// 最终结果: 1 2 4 3
async function test () {
    console.log(1);
    await 123
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 最终结果: 1 3 2 4 5 6 7

1.4.2 thenable类型

await 后面接 thenable 类型,需要等待一个 then 的时间之后执行

async function test () {
    console.log(1);
    await {
        then (cb) {
            cb();
        },
    };
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 最终结果: 1 3 4 2 5 6 7

1.4.3 Promise类型

1.4.3.1 没有两个then等待

async function test () {
    console.log(1);
    await new Promise((resolve, reject) => {
        resolve()
    })
    console.log(2);
}

test();
console.log(3);

Promise.resolve()
    .then(() => console.log(4))
    .then(() => console.log(5))
    .then(() => console.log(6))
    .then(() => console.log(7));

// 最终结果: 1 3 2 4 5 6 7

为什么表现的和 非 thenable 值一样呢?为什么不等待两个 then 的时间呢?

TC 39(ECMAScript标准制定者)await 后面是 promise 的情况如何处理进行了一次修改,移除了额外的两个微任务,在早期版本,依然会等待两个 then 的时间
有大佬翻译了官方解释:更快的 async 函数和 promises[1],但在这次更新中并没有修改 thenable 的情况
这样做可以极大的优化 await 等待的速度

1.4.3.2 循环交叉输出

async function func () {
    console.log(1);
    await 1;
    console.log(2);
    await 2;
    console.log(3);
    await 3;
    console.log(4);
}

async function test () {
    console.log(5);
    await func();
    console.log(6);
}

test();
console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最终结果: 5 1 7 2 8 3 9 4 10 6 11

awaitPromise.prototype.then 虽然很多时候可以在时间顺序上能等效,但是它们之间有本质的区别。
test 函数中的 await 会等待 func 函数中所有的 await 取得 恢复函数执行 的命令并且整个函数执行完毕后才能获得取得 恢复函数执行的命令;
也就是说,func 函数的 await 此时不能在时间的顺序上等效 then,而要等待到 test 函数完全执行完毕;
比如这里的数字6很晚才输出,如果单纯看成then的话,在下一个微任务队列执行时6就应该作为同步代码输出了才对。
所以我们可以合并两个函数的代码

async function test () {
    console.log(5);
    console.log(1);
    await 1;
    console.log(2);
    await 2;
    console.log(3);
    await 3;
    console.log(4);
    await null;
    
    console.log(6);
}

test();
console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最终结果: 5 1 7 2 8 3 9 4 10 6 11

因为将原本的函数融合,此时的 await 可以等效为 Promise.prototype.then,又完全可以等效如下代码

async function test () {
    console.log(5);
    console.log(1);
    Promise.resolve()
        .then(() => console.log(2))
        .then(() => console.log(3))
        .then(() => console.log(4))
        .then(() => console.log(6))
}

test();
console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最终结果: 5 1 7 2 8 3 9 4 10 6 11

以上三种写法在时间的顺序上完全等效,所以 完全可以将 await 后面的代码可以看做在 then 里面执行的结果,又因为 async 函数会返回 promise 实例,所以还可以等效成

async function test () {
    console.log(5);
    console.log(1);
}

test()
    .then(() => console.log(2))
    .then(() => console.log(3))
    .then(() => console.log(4))
    .then(() => console.log(6))

console.log(7);

Promise.resolve()
    .then(() => console.log(8))
    .then(() => console.log(9))
    .then(() => console.log(10))
    .then(() => console.log(11));

// 最终结果: 5 1 7 2 8 3 9 4 10 6 11

可以发现,test 函数全是走的同步代码...

所以:async/await 是用同步的方式,执行异步操作

1.5 await+sync 示例说明

1.5.1 返回和无返回

async function async2 () {
    new Promise((resolve, reject) => {
        resolve()
    })
}
async function async3 () {
    return new Promise((resolve, reject) => {
        resolve()
    })
}

async function async1 () {
    // 方式一:最终结果:B A C D
    // await new Promise((resolve, reject) => {
    //     resolve()
    // })

    // 方式二:最终结果:B A C D
    // await async2()

    // 方式三:最终结果:B C D A
    await async3()

    console.log('A')
}

async1()

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

大致思路:

  • 首先,async函数的整体返回值永远都是Promise,无论值本身是什么
  • 方式一:await的是Promise,无需等待
  • 方式二:await的是async函数,但是该函数的返回值本身是 非thenable,无需等待
  • 方式三:await的是async函数,且返回值本身是Promise,需等待两个then时间

1.5.2 返回 Promise

function func () {
    console.log(2);

    // 方式一:1 2 4  5 3 6 7
    // Promise.resolve()
    //     .then(() => console.log(5))
    //     .then(() => console.log(6))
    //     .then(() => console.log(7))

    // 方式二:1 2 4  5 6 7 3
    return Promise.resolve()
        .then(() => console.log(5))
        .then(() => console.log(6))
        .then(() => console.log(7))
}

async function test () {
    console.log(1);
    await func();
    console.log(3);
}

test();
console.log(4);

步骤拆分:

  • 方式一:
    同步代码输出1、2,接着将log(5)处的then1加入微任务队列,await拿到确切的func函数返回值undefined,将后续代码放入微任务队列(then2,可以这样理解)
    执行同步代码输出4,到此,所有同步代码完毕
    执行第一个放入的微任务then1输出5,产生log(6)的微任务then3
    执行第二个放入的微任务then2输出3
    然后执行微任务then3,输出6,产生log(7)的微任务then4
    执行then4,输出7
  • 方式二:
    同步代码输出1、2,await拿到func函数返回值,但是并未获得具体的结果(由Promise本身机制决定),暂停执行当前async函数内的代码(跳出、让行)
    输出4,到此,所有同步代码完毕
    await一直等到Promise.resolve().then...执行完成,再放行输出3

继续

function func () {
    console.log(2);

    return Promise.resolve()
        .then(() => console.log(5))
        .then(() => console.log(6))
        .then(() => console.log(7))
}

async function test () {
    console.log(1);
    await func()
    console.log(3);
}

test();
console.log(4);

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

// 最终结果: 1 2 4    B 5 C 6 D 7 3
async function test () {
    console.log(1);
    await Promise.resolve()
        .then(() => console.log(5))
        .then(() => console.log(6))
        .then(() => console.log(7))
    console.log(3);
}

test();
console.log(4);

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

// 最终结果: 1 4    B 5 C 6 D 7 3

综上,await 一定要等到右侧的表达式有确切的值才会放行,否则将一直等待(阻塞当前async函数内的后续代码),不服看看这个

function func () {
  return new Promise((resolve) => {
      console.log('B')
      // resolve() 故意一直保持pending
  })
}

async function test () {
  console.log(1);
  await func()
  console.log(3);
}

test();
console.log(4);
// 最终结果: 1 B 4 (永远不会打印3)


// ---------------------或者写为-------------------
async function test () {
  console.log(1);
  await new Promise((resolve) => {
      console.log('B')
      // resolve() 故意一直保持pending
  })
  console.log(3);
}

test();
console.log(4);
// 最终结果: 1 B 4 (永远不会打印3)

1.5.3 示例三

async function func () {
    console.log(2);
    return {
        then (cb) {
            cb()
        }
    }
}

async function test () {
    console.log(1);
    await func();
    console.log(3);
}

test();
console.log(4);

new Promise((resolve) => {
    console.log('B')
    resolve()
}).then(() => {
    console.log('C')
}).then(() => {
    console.log('D')
})

// 最终结果: 1 2 4 B C 3 D

步骤拆分:
同步代码输出1、2
await拿到func函数的具体返回值thenable,将当前async函数内的后续代码放入微任务then1(但是需要等待一个then时间)
同步代码输出4、B,产生log(C)的微任务then2
由于then1滞后一个then时间,直接执行then2输出C,产生log(D)的微任务then3
执行原本滞后一个then时间的微任务then1,输出3
执行最后一个微任务then3输出D

1.6 总结

async函数返回值

  • 结论:async函数在抛出返回值时,会根据返回值类型开启不同数目的微任务
    • return结果值:非thenable非promise不等待
    • return结果值:thenable(等待 1个then 的时间)
    • return结果值:promise(等待 2个then 的时间)
  • await右值类型区别
    • 非 thenable 类型,会立即向微任务队列添加一个微任务then,但不需等待
    • thenable 类型,需要等待一个 then 的时间之后执行
    • Promise类型(有确定的返回值),会立即向微任务队列添加一个微任务then,但不需等待

参考资料
https://juejin.cn/post/6844903715342647310#heading-3: https://juejin.cn/post/6844903715342647310#heading-3

标签:resolve,console,log,await,JS,Promise,async
From: https://www.cnblogs.com/jingzh/p/18327375

相关文章