首页/文章/javascript

你不知道的 async、await 魔鬼细节

2024-08-06
22858 分钟
...

0、前言

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

试一试?

🌰1:

`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

`

🌰2:

`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、async 函数返回值

在讨论 await 之前,先聊一下 async 函数处理返回值的问题,它会像 Promise.prototype.then 一样,会对返回值的类型进行辨识。

👉根据返回值的类型,引起 js引擎 对返回值处理方式的不同

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

  • return结果值:非thenable、非promise(不等待)

  • return结果值:thenable(等待 1个then的时间)

  • return结果值:promise(等待 2个then的时间)

🌰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:

`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:

`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

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

`

看了这三个🌰是不是对上面的结论有了更深的认识?

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

`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

`

👀做错了吧?

哈哈没关系

步骤拆分👇:

  1. 先执行同步代码,输出5

  2. 执行setTimeout,是放入宏任务异步队列中

  3. 接着执行async1函数,输出1

  4. 执行async2函数,输出3

  5. Promise构造器中代码属于同步代码,输出4

    async2函数的返回值是Promise,等待2then后放行,所以AAA暂时无法输出

  6. async1函数暂时结束,继续往下走,输出7

  7. 同步代码,输出11

  8. 执行第一个then,输出8

  9. 执行第二个then,输出9

  10. 终于到了两个then执行完毕,执行async1函数里面剩下的,输出AAA

  11. 再执行最后一个微任务then,输出10

  12. 执行最后的宏任务setTimeout,输出6

❓是不是豁然开朗,欢迎点赞收藏!

2、await 右值类型区别

2.1、非 thenable

🌰1:

`async function test () {
    console.log(1);
    await 1;
    console.log(2);
}

test();
console.log(3);
// 最终结果👉: 1 3 2

`

🌰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

`

🌰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

`

Note:

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

2.2、thenable类型

`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

`

Note:

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

2.3、Promise类型

`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 的时间呢?

Note:

  • TC 39(ECMAScript标准制定者) 对await 后面是 promise 的情况如何处理进行了一次修改,移除了额外的两个微任务,在早期版本,依然会等待两个 then 的时间

  • 有大佬翻译了官方解释:更快的 async 函数和 promises[1],但在这次更新中并没有修改 thenable 的情况


这样做可以极大的优化 await 等待的速度👇

`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

`

Note:

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 是用同步的方式,执行异步操作**

3、🌰

🌰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时间

🌰2:

`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、2await拿到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

`

Note:

综上,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)

    `

🌰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

4、总结

async函数返回值

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

  • return结果值:非thenable、非promise(不等待)

  • return结果值:thenable(等待 1个then的时间)

  • return结果值:promise(等待 2个then的时间)

await右值类型区别

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

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

  • Promise类型(有确定的返回值),会立即向微任务队列添加一个微任务then但不需等待

  • TC 39 对await 后面是 promise 的情况如何处理进行了一次修改,移除了额外的两个微任务,在早期版本,依然会等待两个 then 的时间

参考资料

[1]

https://juejin.cn/post/6844903715342647310#heading-3: https://juejin.cn/post/6844903715342647310#heading-3

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-10
手把手教你实现一个EventEmitter
你是不是也遇到过这种情况:代码里各种事件监听和触发,回调函数套了一层又一层,最后自己都理不清哪个事件先触发、哪个后触发了? 别担心,今天我就带你从零开始实现一个自己的事件触发器(EventEmitter),让你彻底掌握事件驱动的精髓,代码清晰度直接提升200%! 为什么要懂EventEmitter? 先说说为什么我们要关心这个。Node.js的核心就是事件驱动,比如文件读写、网络请求,都是通过事件来处理的。不会EventEmitter,...
学习面试
javascript2025-08-09
Web Worker 多线程魔法
你是否曾遇到过网页因为复杂计算而卡顿的情况?今天,让我们一起探索Web Worker的神奇世界,看看如何用多线程技术解决性能瓶颈,并实现流畅的图片压缩功能! 为什么需要Web Worker? 想象一下,你在一个繁忙的餐厅里,只有一位服务员(主线程)负责点餐、上菜、结账所有工作。当客人突然暴增时,这位服务员就会手忙脚乱,其他客人只能干等着… 这就是JavaScript的现状—— 单线程 !所有任务都在一个线程中排队执行,一旦遇到复杂计算(...
学习面试
javascript2025-08-07
🌐AST抽象语法树
AST 是前端开发中一个非常重要的概念,它就像是代码的“编译器”,能够帮助我们理解和操作代码。本文将深入探讨 AST 的原理和应用,带你从零开始了解 AST。 一、什么是 AST? AST(抽象语法树)是一种树形结构,用于表示程序代码的语法结构。它将代码分解为一系列的节点,每个节点代表代码中的一个元素,如变量、函数、表达式等。AST 是编译器和解释器的基础,也是代码分析和转换的关键工具。 1\. AST 的结构 AST 的结构通常包括以...
学习面试
javascript2025-08-05
打字机效果是如何实现的
分析效果 使用 AI 对话的时候,文字是一个字一个字蹦出来的?它不是等所有内容生成完毕后一次性展示,而是以一种流畅、实时的“打字机”效果呈现,这种体验的背后,隐藏着一种高效的Web技术。它就是 服务器推送事件(Server Sent Events,简称 SSE) 什么是 SSE 从根本上说,SSE 是一种允许服务器 单向 、 持续 地向客户端(浏览器)推送数据的Web技术。 可以把它想象成一个 电台广播 : 服务器 是广播电台,持续不断...
学习面试
javascript2025-08-04
Promise 从原理到实现
前言 是 异步编程的核心概念之一,它为我们提供了一种更优雅的方式来处理异步操作。 本文将全面剖析 的工作原理,并逐步实现一个符合 规范的 。 正文 一、Promise 基础概念 代表一个异步操作的最终完成(或失败)及其结果值。它有 种状态: 1. pending : ,既不是成功,也不是失败状态 2. fulfilled :意味着操作 完成 3. rejected :意味着操作 状态一旦改变就不能再变,只能从 变为 ,或从 变为 。 二...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录