这两年我看过市面上几乎所有 Promise 教程,但始终是一知半解。直到我搞清楚了浏览器多线程、事件循环和微任务,才迎来真正的顿悟。

前言:那些年我踩过的坑

Promise 的重要性不言而喻,可就是学不会。我看过阮一峰老师的教程,刷过各种视频,甚至能背出"链式调用、解决回调地狱"这些概念,但一碰到执行顺序的题目就懵。

最大的误解是什么?我以前一直以为"异步"就是 JS单线程同时处理多件事情,好像它能一边倒计时一边执行下面的代码。这种错误认知让我在面对 setTimeoutPromise 混用的代码时永远答不对。

直到我读了一篇文章 —— 《从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理》,配合阮一峰的 Promise 教程重新学习,我才真正开了窍。

下面我把这次"顿悟"的过程完整梳理出来,希望能帮助那些和我一样曾经痛苦挣扎的同学。


一、核心突破:理解"单线程异步"的真正含义

误解 vs 真相

我曾经的误解真正的机制
JS 自己一边倒计时一边执行后面的代码JS 把定时任务交给浏览器的定时器线程去处理
异步就是单线程来回切换干不同的事异步是发布订阅模式:订阅事件 → 别的线程执行 → 完成后把回调放回任务队列
Promise 构造函数里的代码也是异步的Promise 构造函数内的代码同步执行,只有 .then 回调才是微任务

核心记忆:JavaScript 引擎是单线程的,但浏览器是多线程的。

当 JS 遇到 setTimeout(cb, 1000) 时,它不会自己倒计时。它会说:“定时器线程,请你在 1 秒后把这个 cb 函数放回任务队列。“然后自己立刻继续执行后续同步代码。定时器线程倒计时结束后,把 cb 塞进宏任务队列,等待主线程空闲时通过事件循环(Event Loop)取走执行。

这就是你理解的 “Promise 中去使用 event loop 以外的线程,然后发布订阅模式” —— 完全正确。


二、事件循环(Event Loop)与任务队列

一旦理解了上面的外包机制,事件循环就变得非常直观。

1. 两类任务

  • 宏任务(MacroTask):整体 script 代码、setTimeoutsetInterval、I/O、UI 渲染。
  • 微任务(MicroTask)Promise.then()Promise.catch()MutationObserver

2. Event Loop 执行顺序

  1. 执行一个宏任务(最开始就是整个 script 代码)。
  2. 执行过程中产生的所有微任务,放入微任务队列。
  3. 当前宏任务执行完毕后,立刻清空整个微任务队列
  4. 进行必要的 UI 渲染。
  5. 从宏任务队列中取出下一个宏任务,循环往复。

关键点:微任务的优先级永远高于宏任务。先清空微任务,再取下一个宏任务。

3. 一个验证你理解的例子

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

输出顺序:1 4 3 2

执行过程:

  • 输出 1(同步代码)
  • setTimeout 交给定时器线程,0ms 后将回调放入宏任务队列
  • Promise.then 的回调放入微任务队列
  • 输出 4(同步代码)
  • 当前宏任务(script)结束,清空微任务队列 → 输出 3
  • 取出下一个宏任务(setTimeout 回调)→ 输出 2

如果你之前答错,现在应该能轻松答对了。


三、从 Event Loop 重新理解 Promise

1. Promise 构造函数是同步执行的

很多初学者以为 new Promise 里的代码是异步的,其实大错特错。

console.log('start');
new Promise((resolve) => {
  console.log('promise executor');
  resolve();
  console.log('after resolve');
}).then(() => {
  console.log('then callback');
});
console.log('end');

输出:

start
promise executor
after resolve
end
then callback

可以看到,resolve() 后面的 console.log('after resolve') 依然同步执行。.then 的回调才是真正的异步(微任务)。

2. 为什么 Promise 可以处理异步操作?

当我们在 Promise 里放置一个 setTimeout 或 AJAX 请求时,真正的异步操作是由浏览器其他线程完成的。Promise 只是提供了一个容器,让我们可以优雅地订阅成功/失败的回调。

new Promise((resolve) => {
  setTimeout(() => {
    resolve('done');
  }, 1000);
}).then((value) => {
  console.log(value);
});
  • setTimeout 被交给定时器线程
  • JS 主线程继续往下执行
  • 1 秒后定时器线程将 resolve 的执行放入任务队列
  • 执行 resolve,触发 .then 回调(微任务)

这就是你所说的 “Promise 本质是使用 event loop 以外的线程 + 发布订阅模式”


四、Promise vs async/await:核心区别

你观察到的区别非常敏锐:

“async/await 函数体内 await 之后的函数都是异步,但 Promise 异步函数后面的函数还会同步执行。”

我们来用代码说清楚。

1. Promise 版本

function promiseDemo() {
  console.log('A');
  Promise.resolve().then(() => {
    console.log('B');
  });
  console.log('C');
}
promiseDemo(); // 输出:A C B
  • AC 是同步输出
  • B.then 微任务,在同步代码执行完毕后输出

2. async/await 版本

async function asyncDemo() {
  console.log('A');
  await Promise.resolve();
  console.log('B');
  console.log('C');
}
asyncDemo();
console.log('D');

输出:A D B C

执行过程:

  • 调用 asyncDemo(),同步执行 console.log('A')
  • 遇到 await,立即暂停当前 async 函数,await 后面的代码被包装成微任务
  • JS 引擎跳出 async 函数,执行外面的同步代码 console.log('D')
  • 同步代码执行完毕,执行微任务队列中的内容,恢复 async 函数执行 B C

所以你的总结完全正确:await 之后的代码全部变为异步(微任务)执行,而 Promise 构造函数内的代码(包括 resolve 之后的代码)依然是同步执行。

3. 对比表格

比较项Promiseasync/await
构造函数/await前的代码同步执行同步执行
异步操作后的代码.then 回调是微任务await 后面所有代码都是微任务
实现关系基础 APIPromise 的语法糖
错误处理.catch()try/catch
阅读体验链式调用近似同步,更直观

五、我的学习路径总结(供参考)

如果你还在混沌中,可以按我走过的路来一遍:

  1. 先理解浏览器多线程模型 明白 JS 引擎线程、定时器线程、HTTP 线程是分开的。异步本质是"外包 + 回调队列”。推荐阅读:从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

  2. 掌握事件循环(Event Loop)和宏任务/微任务 这是分析一切异步代码的底层工具。多画流程图,手动执行几道经典题。

  3. 重读 Promise 教程(推荐阮一峰 ES6 入门) 在理解了事件循环的基础上,你会发现 Promise 的所有行为都变得合理可预测。

  4. 刻意练习 找 10 道混合 setTimeoutPromise.thenasync/await 的输出顺序题,手写答案并验证。这个过程能帮你彻底固化理解。


补充:两个被你验证过的重要认知

随着学习的深入,有两个结论值得单独提炼出来。它们能帮你建立更稳定的心智模型,也能在日常开发中减少不必要的纠结。

1. 默认同步原则:除非主动写异步,否则所有代码都是同步的

我们书写的绝大多数代码,都运行在 JS 主线程上,并且是同步执行的。 不会出现"JS 自动把某段代码变成异步"这种魔法。

// 下面的每一行都是同步执行,从上到下,一行一行完成
let a = 1;
let b = a + 2;
console.log(b); // 立即输出 3

function add(x, y) {
  return x + y;
}
let c = add(5, 6); // 同步调用,立即得到结果
console.log(c); // 立即输出 11

只有在主动调用特定 API 时,才会产生异步行为:

  • 定时器:setTimeoutsetInterval
  • 网络请求:fetchXMLHttpRequest
  • Promise 相关:.then / .catch / .finallyasync/await
  • 事件监听:addEventListener(回调是宏任务)
  • Node.js 中的文件操作:fs.readFilefs.promises.readFile

一句话记住:

正常写代码 = 同步;主动调异步 API = 异步。

2. 按需异步原则:分清微任务/宏任务不是必须的,只在必要时才深究

很多初学者被"微任务、宏任务、事件循环"吓到,以为写每一行代码都要先分析一下。其实完全不必。

  • 日常业务开发中,你只需要记住:

    • Promise.then / catch / finally 以及 await 后面的代码,会在当前同步代码执行完后尽快执行(比 setTimeout(fn,0) 快)。
    • setTimeoutsetInterval、事件回调、HTTP 回调等,会稍后执行(需要排队,等微任务清空)。
  • 你不需要对所有 API 做精确分类,也无需时刻思考事件循环。只要代码按你预期的顺序工作,就放心写。

什么时候需要深入区分微任务/宏任务?

  • 代码执行顺序不符合预期(例如 setTimeout 的回调比一个 Promise .then 先跑了)。
  • 面试或刷题中被问到"输出顺序”。
  • 你在写一个复杂的状态机或协调多个异步操作的库。

实用建议:

  • 当你需要一个异步操作时,直接用 async/await(它基于 Promise,行为更直觉)。
  • 如果你想知道"这段异步代码什么时候执行",就记住一条:微任务(Promise.then/await 后续)总是在当前同步代码结束后、下一个宏任务(setTimeout 等)之前执行。 这就覆盖了 95% 的场景。

写在最后

Promise 并不难,难的是我们一开始用了错误的心智模型去理解它。一旦建立起"浏览器多线程 + 事件循环 + 微任务/宏任务"的正确框架,Promise 和 async/await 就变成了自然而然的结果。

希望这篇文章能帮你少走一些弯路。如果你有疑问或不同见解,欢迎留言讨论。

“所有异步操作本质是 Promise 中去使用 event loop 以外的线程,然后发布订阅模式。” —— 这句话,是我从看了两年教程到真正理解的那一刻,脑海里蹦出的总结。也送给你。