你是否曾经疑惑过:为什么 setTimeout 后面的代码会先执行?为什么 Promise 能解决回调地狱?JS 明明是单线程,却能处理网络请求、定时任务?本文将带你从计算机底层进程线程讲起,逐步揭开 JS 异步机制的神秘面纱,彻底掌握 Promise 的核心用法
一、进程与线程:先搞懂"董事长"和"经理"
在了解 JS 之前,我们需要先认识两个操作系统层面的概念:进程 和线程。
- 进程(Process) :可以理解为一个正在运行的程序实例。操作系统会为它分配独立的内存空间、文件句柄等资源。每个进程都有一个唯一的
PID(进程 ID)。就像一个董事长,负责统筹全局、分配资源。 - 线程(Thread) :是进程内的执行单元,一个进程至少有一个线程(主线程)。线程共享进程的资源,负责具体的代码执行。就像经理,在董事长的资源支持下,真正去干活。
C++、Java 这类系统级语言,支持多进程多线程架构。它们可以同时开启多个线程并行执行任务,效率极高,但复杂度也高(需要处理锁、竞态条件等)。
而 JavaScript 的设计理念恰恰相反:简单、易用、避免复杂性。
二、JS 为什么是单线程?
浏览器中的 JavaScript 最初设计用于操作 DOM、响应用户交互。如果支持多线程同时修改同一个 DOM 节点,就会出现冲突(比如一个线程删除节点,另一个线程修改其样式)。为了避免这种复杂性,JS 选择了一条最简单的路------单线程。
所谓单线程,意味着 JS 引擎在一个时刻只能执行一段代码,所有的任务都需要排队执行。
但是,单线程带来了一个严重问题:如果某段代码执行时间很长(比如网络请求、定时任务),后续代码就会被阻塞,页面会"卡死"。这显然无法接受。
于是,JS 的设计者引入了异步机制:把那些耗时操作(定时器、网络请求、事件监听)交给浏览器或 Node.js 的底层去处理,等它们完成后再把回调函数放回队列中执行。这样,主线程永远不会被阻塞。
三、JS 执行机制:同步优先,异步靠"事件循环"
JS 代码执行时,究竟发生了什么?我们来看一个最经典的例子(对应 1.js):
javascript
// 同步代码
console.log('start');
// 异步代码
setTimeout(() => {
console.log('222');
}, 1000)
console.log('end');
输出顺序是什么?
答案是:start → end → (1秒后) 222
为什么会这样?背后的执行机制如下:
- 启动进程:JS 引擎启动一个进程,分配资源。
- 开启主线程:主线程开始执行代码。
- 同步任务优先 :遇到
console.log('start'),立即执行。遇到setTimeout,浏览器会将其定时器交给 Web APIs(底层模块)处理,并把它的回调函数放入任务队列(Task Queue) 中,主线程继续往下执行。 - 继续执行同步任务 :执行
console.log('end')。 - 事件循环(Event Loop)登场 :当主线程中的所有同步代码执行完毕后,事件循环会反复检查任务队列,一旦队列中有任务,就将其取出放到主线程执行。
- 执行异步回调 :1秒后,定时器触发,回调函数被推入任务队列,事件循环将它取出,执行
console.log('222')。
核心口诀:先同步,后异步;同步跑完,再看队列。
下面这张图可以帮助你记忆:
scss
┌─────────────┐ ┌─────────────┐
│ 调用栈 │◄─────│ 任务队列 │
│ (执行同步码) │ │ (存放回调) │
└─────────────┘ └─────────────┘
▲ │
│ │
└────事件循环(Event Loop)────┘
更深入:宏任务与微任务
JS 中的异步任务其实分为两种队列:
- 宏任务(MacroTask) :
setTimeout、setInterval、I/O 操作、UI 渲染等。 - 微任务(MicroTask) :
Promise.then/catch/finally、MutationObserver等。
执行顺序:每执行完一个宏任务,就会清空当前所有的微任务,然后再取下一个宏任务。
javascript
setTimeout(() => console.log('宏任务1'), 0);
Promise.resolve().then(() => console.log('微任务1'));
console.log('同步代码');
// 输出:同步代码 → 微任务1 → 宏任务1
理解这个顺序,对后面学习 Promise 非常有帮助。
四、异步流程控制:为什么需要 Promise?
假设我们有这样一个需求:先请求用户列表(A),拿到每个用户的 id 后再去请求详细信息(B)。如果用传统的回调嵌套,就会出现"回调地狱":
javascript
getUsers((users) => {
users.forEach(user => {
getUserDetail(user.id, (detail) => {
console.log(detail);
})
})
})
代码横向发展,难以维护。这时,ES6 引入的 Promise 闪亮登场,优雅地解决了异步流程控制问题。
五、Promise 完全指南
5.1 什么是 Promise?
Promise 是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。它提供统一的 API 来处理成功和失败的情况。
5.2 基本用法(对应 3.js)
javascript
const p = new Promise((resolve, reject) => {
console.log('许诺言'); // 这句会立即执行!
// 里面可以放耗时任务,比如定时器、fetch等
setTimeout(() => {
// 成功时调用 resolve
// resolve(666);
// 失败时调用 reject
reject("网络错误");
}, 2000)
});
console.log(p.__proto__); // 打印 Promise 原型
p
.then((data) => {
console.log(data); // 如果 resolve 被调用,这里会收到 666
console.log('end');
})
.catch((err) => {
console.log(err); // 输出 "网络错误"
})
.finally(() => {
console.log('finally'); // 无论成功失败都会执行
});
关键点解析:
- executor 函数 :
new Promise(executor)中的参数,会立即同步执行 ,所以'许诺言'会第一时间打印。 resolve和reject:它们是两个函数,由 JS 引擎提供。当异步任务成功时,调用resolve(result),会将结果传递给then回调;失败时调用reject(error),将错误传递给catch。then和catch:返回的都是新的Promise对象,支持链式调用。- 状态变化 :
Promise有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变,就不会再变。
5.3 实际场景:使用 fetch(对应 4.html)
javascript
console.log('start');
// fetch 底层就是 Promise
fetch('https://api.deepseek.com/chat/completions', {
method: 'post'
}).then((response) => {
return response.json(); // 又是一个 Promise
}).then((data) => {
console.log(data);
}).catch((err) => {
console.log(err);
});
console.log('end');
// 输出:start → end → (请求完成后输出 data 或 err)
fetch 发起网络请求是异步的,不会阻塞主线程,所以 'end' 会先打印。
5.4 手写 sleep 函数 ------ 用 Promise 封装 setTimeout(对应 5.html)
JS 本身没有内置 sleep 函数,但我们可以用 Promise 轻松实现:
javascript
function sleep(t) {
return new Promise((resolve, reject) => {
console.log('同步代码:Promise 里的 executor 立即执行');
setTimeout(() => {
resolve(); // 延迟 t 毫秒后,调用 resolve 让 Promise 成功
}, t);
});
}
// 使用 sleep
sleep(2000)
.then(() => {
console.log('2s 后再做');
});
输出:
同步代码:Promise 里的 executor 立即执行 → (等待 2 秒) → 2s 后再做
这样,我们就创造了一个"延时执行"的控制流,而且不会阻塞主线程!
六、综合示例:同步异步混合场景
下面这段代码融合了同步代码、setTimeout、Promise 微任务,试着预测输出顺序:
javascript
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
输出顺序:1 → 4 → 3 → 2
解析:
- 同步代码
1和4先执行。 - 同步执行完毕后,事件循环检查微任务队列,发现
Promise.then的回调,执行打印3。 - 微任务清空后,再取下一个宏任务(
setTimeout),打印2。
掌握这个顺序,你就真正理解了 JS 的事件循环机制!
七、总结与心得
| 概念 | 核心要点 |
|---|---|
| 进程与线程 | 进程是资源分配单位(董事长),线程是执行单位(经理)。JS 单线程简化了开发。 |
| JS 执行机制 | 同步任务优先执行,异步任务交给底层,完成后将回调放入任务队列,事件循环不断取出执行。 |
| 宏任务与微任务 | 每个宏任务执行完都会清空微任务队列。Promise 相关回调属于微任务。 |
| Promise | 容器 + 状态机,通过 resolve/reject 改变状态,用 then/catch 注册回调,优雅解决异步流程控制。 |
| 手写 sleep | new Promise(resolve => setTimeout(resolve, t)) 即可实现。 |
希望这篇文章能帮你彻底打通 JS 异步的任督二脉。如果在实际项目中遇到复杂的异步依赖,不妨先画一下流程图,然后用 Promise 链式调用或 async/await 去实现。