第 42 期 - JavaScript 的 async/await、事件循环与 Promise 面试题解析
摘要
本文先阐述 async/await 与普通函数的区别,包括返回值和异常处理,再介绍操作系统的进程与线程概念,然后讲述浏览器中的 JavaScript 线程和事件循环,区分宏任务与微任务队列,最后通过 Promise 执行面试题巩固知识,并提及 Node 事件循环的相关内容。
一、async/await 相关内容
1. async 函数与普通函数的区别
- 函数声明方式:async 函数有三种基础写法,分别是
async function
、箭头函数前加async
、类中的方法加async
。它和生成器函数写法有相似之处,但async
位置不同。 - 执行方式:
- 起初推测
async
函数要么无法正常执行,要么函数体内内容异步,但实际代码测试发现和普通函数调用一样,函数体内容按顺序执行。 - 返回值区别:
async
函数返回值是一个封装后的Promise
,这是与普通函数的重要区别之一。- 当返回普通值时,相当于被包裹到
Promise.resolve
中;返回Promise
时,状态由Promise
决定;返回对象且实现了thenable
,由对象的then
方法决定。
- 异常处理区别:普通函数抛出异常直接输出堆栈跟踪错误信息且不再执行,而
async
异步函数异常会进行Promise.reject(error)
操作,可被Promise
的catch
方法捕获。
- 起初推测
2. async 函数中的关键字 await
- 作用及特性:
await
只能在async
异步函数内使用,其功能类似生成器函数中的yield
,意为等待异步操作(通常是Promise
)完成。它会暂停函数执行,直到Promise
变为fulfilled
状态才继续,并且直接将Promise
的结果作为返回值,形成同步代码视觉效果。 - 返回值情况:
await
后面可跟任意值、异步的Promise
或者Thenable
对象。若Promise
正常返回则获取处理结果,若为reject
状态则作为函数Promise
的reject
值;若等待的值不满足thenable
,则返回表达式本身的值。 - 具体示例:如
async
函数中的await
后面跟网络请求(返回Promise
),以及跟普通值、Thenable
对象等不同情况的示例展示了其返回值特点。
二、操作系统的进程与线程概念
1. 进程与线程的基本概念
- 进程:计算机已运行的程序,是操作系统管理程序的一种方式,启动一个应用程序可能会启动一个或多个进程。
- 线程:操作系统能够运行运算调度的最小单位,通常包含在进程中。进程像是线程的容器,每个进程至少有一个线程(主线程)来执行程序代码。
- 二者区别:进程负责资源初步分配,资源都在进程身上,所以进程较重,切换开销大;线程基本不拥有资源,只具备运行所需的部分,与同一进程下的其他线程共享进程资源,所以线程很轻,切换开销小。
2. 操作系统的工作方式
- 操作系统通过 CPU 在多个进程间快速切换,让多个进程看似同时工作。当进程中的线程获取到时间片(CPU 资源运行时间)时,就可以执行代码,用户感受不到这种切换。
三、浏览器中的 JavaScript 线程和事件循环
1. 浏览器中的 JavaScript 线程
- 多数浏览器是多进程的,打开一个 tab 页面会开启一个新进程,每个进程有很多线程,其中包括执行 JavaScript 代码的线程。JavaScript 代码在单独线程执行,同一时刻只能做一件事,耗时操作不由 JavaScript 线程执行(如网络请求、定时器等由浏览器其他线程完成),JavaScript 线程专注于代码运行。
2. 事件循环
- 认识事件循环:
- 完整流程包括执行代码脚本、栈操作(同步代码)、推送耗时操作给浏览器其余线程、推送到事件队列、任务处理(宏微任务),并且 4 与 5 循环重复以保持应用持续响应外部事件。
- 例如
setTimeout
函数本身是同步函数,其回调函数是异步的,会被传给浏览器其他线程计时,计时结束后交回给 JS 线程执行。
- 宏任务与微任务:
- 队列区分:事件队列可细分为宏任务队列(如
ajax
、setTimeout
、setInterval
、DOM
监听、UI Rendering
等)和微任务队列(如Promise
的then
回调、Mutation Observer API
、queueMicrotask()
等)。 - 区分意义:细分为两个队列可优先处理更紧急操作,提高应用响应性避免界面阻塞。宏任务执行前要确保微任务队列已清空。
- 队列区分:事件队列可细分为宏任务队列(如
四、Promise 执行面试题
1. 面试题一
- 存在多种类型代码(定时器、
Promise
、queueMicrotask
、main script
代码)交缠的情况,根据代码类型确定优先度执行顺序为同步代码执行、微任务执行、宏任务执行。 - 其中定时器是宏任务,
Promise
的then
方法和queueMicrotask
是微任务,main script
是同步代码。并且对于复杂的宏任务内嵌套微任务的情况,要遵循宏微任务基本规范(执行宏任务之前,所有微任务必须清空)来确定执行顺序。
2. 面试题二
- 涉及
async/await
的题目,async
函数中await
之外的内容为同步代码,await
返回的结果是Promise
(归属于微任务队列)。 - 根据执行顺序进行基础排序,同步代码先执行,然后是微任务代码,最后是宏任务代码。要注意
await
在async
函数中的暂停效果以及后续代码的归属队列情况。
3. 面试题三
- 本题考察对
Promise
状态调度的熟悉程度,存在多种容易出错的情况。 - 看似简单按顺序排列的结果是错误的,需要深入分析
Promise
链的执行情况,如Promise.resolve
的简写形式对执行顺序的影响,返回不同类型值(普通值、thenable
、Promise
)时then
方法的执行顺序变化,这涉及到微任务队列的特性以及同步代码与微任务的交互情况。
4. 宏任务执行顺序示例
- 以两个
script
标签中的宏任务和定时器宏任务为例,宏任务会先整体 push 到宏任务事件队列中,执行顺序不是按照简单的嵌套顺序,而是有其特定的规则,体现了宏任务与微任务的不同特点。
五、Node 事件循环
1. Node 事件循环的阶段
- 在 Node 进程中,
JS
线程执行JS
代码,耗时操作交给 Node 中的其他线程,结束后加入队列,JS
引擎从队列获取内容。 - 一次完整的事件循环
Tick
分成多个阶段,包括定时器、待定回调、idle
和prepare
、轮询、检测、关闭的回调函数等阶段,JS
线程挨个在队列中找内容执行,执行完所有队列后一次Tick
结束并不断循环。
2. Node 的宏任务与微任务
- Node 的事件循环也分微任务和宏任务,宏任务包含
setTimeout
、setInterval
、IO
事件、setImmediate
、close
事件等;微任务包含Promise
的then
回调、process.nextTick
、queueMicrotask
。 - Node 中的事件循环不只是微任务队列和宏任务队列,还包含不同功能的多个队列用于处理不同类型的任务。
扩展阅读
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有