前言
在现代 Web 开发中,异步编程是绕不开的核心话题。无论是读取文件、发起网络请求,还是处理用户交互,JavaScript 都需要在单线程的限制下高效完成任务。为此,语言提供了 Promise、async/await 等工具,让“异步代码”看起来像“同步代码”一样清晰。
然而,许多初学者甚至中级开发者仍对以下问题感到困惑:
- 为什么
new Promise中的console.log()是同步执行的? - 被
async声明的函数为什么返回一个Promise? - 异步代码的执行顺序到底如何?
本文将从底层机制出发,结合真实代码示例,彻底讲清 JavaScript 异步函数的执行逻辑,并揭示其背后的运行时模型。
一、JavaScript 是单线程,但支持异步
1. 单线程 ≠ 不能并发
- JavaScript 只有一个主线程,负责执行代码。
- 但浏览器或 Node.js 提供了多线程能力(如网络线程、定时器线程、文件 I/O 线程)。
- 当遇到耗时操作(如
fetch、fs.readFile、setTimeout),JS 主线程会将其委托给宿主环境,自己继续执行后续同步代码。
2. Event Loop:异步任务的调度中心
- 同步代码 → 直接执行。
- 异步任务 → 放入 Event Loop(事件循环) 的任务队列。
- 当所有同步代码执行完毕,Event Loop 才会将队列中的回调函数推入主线程执行。
✅ 这就是为什么
console.log(1)、console.log(2)总是在异步回调之前输出。
二、Promise:异步流程的控制者
1. new Promise 的执行时机
看这段代码:
js
编辑
console.log(1);
const p = new Promise((resolve, reject) => {
console.log(3); // ← 为什么这是同步的?
fs.readFile('./a.txt', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
p.then(data => console.log('then:', data));
console.log(2);
输出顺序:1 → 3 → 2 → then: ...
🔍 关键点解析:
new Promise(executor)会立即调用executor函数(即传入的那个箭头函数)。- 因此,
console.log(3)是在同步阶段执行的。 - 但
fs.readFile是异步 I/O 操作,它的回调被放入 Event Loop。 resolve()被调用时,才会将.then()的回调放入微任务队列(Microtask Queue)。
💡 结论:
Promise构造函数内部的同步代码是立即执行的,只有异步回调才是延迟执行的。
2. 微任务 vs 宏任务
-
.then()、.catch()属于 微任务(Microtask) -
setTimeout、setInterval、I/O 回调属于 宏任务(Macrotask) -
Event Loop 执行顺序:
- 执行当前宏任务(如同步代码)
- 清空所有微任务队列
- 执行下一个宏任务
这解释了为什么 Promise.then 总是比 setTimeout 更早执行。
三、async/await:语法糖下的 Promise
1. async 函数的本质
js
编辑
async function fetchData() {
console.log('start');
const res = await fetch('/api/data');
console.log('end');
return res;
}
等价于:
js
编辑
function fetchData() {
console.log('start');
return fetch('/api/data').then(res => {
console.log('end');
return res;
});
}
✅ 核心特性:
- 任何
async函数都会返回一个Promise对象 await只能在async函数内部使用await后面可以跟Promise或普通值(普通值会被包装成 resolved Promise)
2. 为什么 async 函数是“异步”的?
虽然 async 函数内部的同步代码会立即执行,但函数本身不会阻塞主线程。
js
编辑
console.log(1);
async function foo() {
console.log(2); // 同步执行
await Promise.resolve();
console.log(3); // 微任务中执行
}
foo();
console.log(4);
输出:1 → 2 → 4 → 3
⚠️ 注意:
console.log(2)是同步的,但console.log(3)被推迟到微任务阶段。
四、常见误区澄清
| 误区 | 正确理解 |
|---|---|
| “Promise 是异步的” | Promise 构造函数内的同步代码是立即执行的 |
| “async 函数会延迟执行” | async 函数体内的同步部分立刻执行,只有 await 后的内容被推迟 |
| “await 会让整个程序暂停” | 不会!它只是让当前函数暂停,主线程继续执行其他同步代码 |
五、大厂高频面试题
🔹 面试题 1:以下代码输出什么?为什么?
js
编辑
console.log('A');
new Promise(resolve => {
console.log('B');
resolve();
}).then(() => console.log('C'));
setTimeout(() => console.log('D'), 0);
console.log('E');
答案:A → B → E → C → D
考点:同步代码 → 微任务(Promise.then)→ 宏任务(setTimeout)
🔹 面试题 2:async 函数返回的是什么?如何获取它的结果?
答案:
- 返回一个
Promise对象。 - 可通过
.then()或外层await获取结果。
js
编辑
async function fn() { return 42; }
fn().then(val => console.log(val)); // 42
🔹 面试题 3:为什么 new Promise 中的 console.log 是同步的?
答案:
new Promise(executor)会立即执行executor函数。- 只有
executor内部的异步操作(如setTimeout、fetch)才会被推迟。 - 因此,
executor中的同步语句(如console.log)属于当前同步执行栈。
🔹 面试题 4:实现一个 sleep 函数,支持 await sleep(1000)
参考答案:
js
编辑
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用
async function demo() {
console.log('开始');
await sleep(1000);
console.log('1秒后');
}
六、结语
JavaScript 的异步机制看似复杂,实则逻辑严密。理解 Event Loop、微任务/宏任务、Promise 执行时机、async/await 本质,是写出高性能、无 bug 异步代码的基础。
记住:
- Promise 构造函数是同步执行的
- async 函数返回 Promise,内部同步代码立即运行
- 真正的“异步”发生在宿主环境(如网络、文件系统)与 Event Loop 的协作中
掌握这些,你就能在异步编程的海洋中乘风破浪!