从进程线程到 async/await,一文吃透前端异步核心原理
事件循环(Event Loop)是 JavaScript 实现单线程非阻塞异步执行的核心机制,也是浏览器与 Node.js 环境中,JS 代码能够有序执行、处理异步任务(网络请求、定时器、DOM 事件等)的底层逻辑。
本文将从进程、线程的基础概念出发,逐步拆解浏览器渲染机制、V8 引擎单线程模型、Event Loop 事件循环,最终落地到 async/await 的原理与实践,帮助前端开发者建立完整的异步编程知识体系。
一、进程与线程:浏览器的底层基石
1. 基础概念
-
进程:进程就是操作系统中正在运行的一个程序实例,是CPU 运行指令时保存和加载上下文所需的时间与资源集合,是
操作系统资源分配的最小单位。 -
线程:CPU 执行具体指令所需的
最小单位,依附于进程存在,一个进程可以包含多个线程。
2. 浏览器中的进程与线程
我们日常使用浏览器多开 Tab 页,本质上就是为每个 Tab 单独创建一个进程,这样做的好处是:
- 单个 Tab 崩溃不会影响整个浏览器
- 资源隔离更安全,避免恶意页面窃取其他页面数据
而在每个进程内部,又包含多个关键线程:
- 渲染线程:负责页面的 HTML、CSS 解析与布局绘制
- JS 引擎线程:负责解析和执行 JavaScript 代码
-
HTTP 请求线程:处理网络请求(如
Ajax、Fetch) - 事件触发线程、定时器线程等
⚠️ 核心限制:由于 JavaScript 可以直接操作 DOM,为了避免 DOM 渲染冲突,渲染线程与 JS 引擎线程必须互斥,不能同时工作。这也是 JS 执行会阻塞页面渲染的根本原因。
二、V8 引擎:单线程与异步的诞生
V8是Chrome和Node.js所使用的JS引擎,它在执行JS代码时默认只开一个线程。
正是这种单线程特性,催生了JS的异步编程模式:
- 遇到同步任务:直接执行。
- 遇到异步任务:先挂起,存入任务队列,等待同步任务执行完毕后再执行异步任务。
这种“先同步,后异步”的执行流程,就是我们常说的事件循环的基础。
三、Event Loop:微任务与宏任务
1. 任务分类
在异步任务中,又分微任务与宏任务。
微任务:指在异步任务中耗时更短的任务,优先级更高,会在当前同步代码执行完毕后立即执行
常见的微任务有:
Promise.then()-
process.nextTick()(Node.js 环境) -
MutationObserver(浏览器环境)
宏任务:指在异步任务中耗时更长的任务,优先级较低,会在微任务全部清空后才会执行
常见的宏任务有:
- 全局
script代码 -
setTimeout()/setInterval() -
AJAX请求、I/O 操作 - UI 渲染(
UI-rendering)
2. 完整执行顺序
事件循环机制的执行流程可以总结为 4 步:
- 先执行同步代码,执行过程中遇到异步任务,将其存入对应的任务队列,微任务存入微任务队列,宏任务存入宏任务队列
- 同步代码执行完毕后,立即执行微任务队列中的所有任务
- 微任务全部执行结束后,如有需要则执行页面渲染
- 渲染完成后,执行宏任务队列中的任务
这个循环会一直持续,直到所有任务都被处理完毕。
四、async/await
async/await 是 ES2017 引入的语法,本质是 Promise 的替代,让异步代码看起来更像同步代码。
核心规则
-
async:函数前加
async,修饰函数(函数声明 / 表达式 / 箭头函数),表示这是一个异步函数,等价于函数内部自动返回了一个Promise实例对象。- 异步函数的返回值会被自动包装成
Promise(即使你返回普通值,也会变成Promise.resolve(值))。 - 如果函数内部抛出错误,返回的
Promise会变成rejected状态。
- 异步函数的返回值会被自动包装成
-
await:必须配合
async使用,只能在async函数内部使用,作用是等待一个Promise完成(resolve/reject),如果await后面不是Promise对象,它就无法 “等待” 该操作完成。- 等待期间,JS 引擎会暂停当前
async函数的执行,去执行其他代码(不会阻塞主线程)。 - 等
Promise完成后,await会返回Promise的 resolve 值;如果Promise被拒绝(reject),会抛出错误,需要用try/catch捕获。 -
await fn()会把fn()当作同步代码看待,并将await之后的代码加入到微任务队列中,等待当前同步代码和微任务执行完毕后再执行
- 等待期间,JS 引擎会暂停当前
代码示例1:
// async/await 基础用法
async function asyncDemo() {
console.log('1. async 函数内同步代码');
const res = await Promise.resolve('await 结果');
console.log('3. await 之后的代码(微任务)');
console.log('res:', res);
}
console.log('0. 全局同步代码');
asyncDemo();
console.log('2. 全局同步代码结束');
//输出结果:
//0. 全局同步代码
//1. async 函数内同步代码
//2. 全局同步代码结束
//3. await 之后的代码(微任务)
//res: await 结果
上述代码示例表明:async 函数内的同步代码会立即执行,await 之后的代码会被放入微任务队列。
代码示例2:
// async/await 处理异步请求
async function fetchData() {
try {
console.log('开始请求数据');
// 模拟网络请求
const response = await new Promise(resolve => {
setTimeout(() => {
resolve({ data: '用户信息' });
}, 1000);
});
console.log('请求成功:', response.data);
return response.data;
} catch (err) {
console.error('请求失败:', err);
}
}
fetchData().then(data => {
console.log('最终处理数据:', data);
});
console.log('同步代码继续执行');
运行结果:
![]()
可以看到,async/await 让异步代码的写法和同步代码几乎一致,可读性大大提升。
五、总结
从进程、线程到 async/await,我们可以了解:
- 浏览器是多进程多线程架构,每个 Tab 是一个独立进程,内部包含渲染线程、JS 引擎线程等
- V8 引擎是单线程执行 JS,因此诞生了异步编程模型
- Event Loop 是 JS 异步的核心,通过同步代码优先异步代码,微任务优先于宏任务的执行顺序,保证了异步代码的有序执行
- async/await
理解了这些底层原理,有助于我们更好了解JavaScript中的异步编程,实现更复杂高效的功能。