我写的 JS 里 setTimeout、Promise 和同步代码混在一起,打印顺序完全不按我写的来,Promise 居然比 setTimeout 先跑,我对着事件循环排查了大半天的复盘

我写了段混着同步代码、setTimeout、Promise.then 的逻辑,以为按从上到下书写顺序执行,结果打印顺序大跌眼镜:同步代码先全跑完,写在后面的 Promise.then 竟比写在前面、延迟为0的 setTimeout 先执行,依赖顺序的逻辑全乱了。深挖才懂是 JS 的事件循环:JS 单线程,执行规则是同步代码先跑完→清空整个微任务队列(Promise.then/await后/queueMicrotask)→取一个宏任务(setTimeout/IO)→再清空微任务→再取下一个宏任务,微任务优先级高于宏任务;所以 setTimeout(fn,0) 不是立即执行,而是下个宏任务时机且排在所有微任务之后,Promise.then 自然先跑。这篇从事件循环的同步/微任务/宏任务三层模型讲起,到需要顺序用 await/then 显式串联、独立的用 Promise.all 并发的正解、事件循环能解释的其他现象(长任务卡死/await不阻塞线程/渲染时机)、微宏任务速查、经典顺序题逐步推演,以及那句最戳心的——代码书写的顺序和实际执行的顺序可能是两回事,真正理解程序不能只静态读文本,更要在脑中模拟它的执行、理解背后决定何时执行什么的运行时机制。

我写的 JS 里 setTimeout、Promise 和同步代码混在一起,打印顺序完全不按我写的来,Promise 居然比 setTimeout 先跑,我对着事件循环排查了大半天的复盘

这是一个让我对 JavaScript 的"事件循环"彻底搞懂的故事。我有一段逻辑,里面混着普通同步代码、setTimeoutPromise.then。我很自然地以为,代码会按我从上到下书写的顺序执行。可一运行,打印的顺序让我大跌眼镜:完全不按我写的顺序来——所有同步代码先一股脑全跑完;然后,那个我写在后面Promise.then,竟然比写在前面的 setTimeout 先执行!我依赖这个执行顺序的逻辑,全乱了套。我明明把 setTimeout(fn, 0) 写在了 Promise.then 前面,延迟还是 0,它为什么反而后跑?

我顺着"顺序不对"的线索深挖,才终于揭开真相,补上了我对 JavaScript 一个最核心、却最容易似懂非懂的认知漏洞:问题的核心,是 JavaScript 的"事件循环(event loop)"执行模型。我一直想当然地以为,"代码就是从上到下,一行行顺序执行的";可真相是:JavaScript 是单线程的,它通过"事件循环"来调度异步任务,而调度有一套严格的优先级,绝不是按书写顺序。执行的规则是:① 先同步执行当前所有的同步代码(把调用栈跑空);② 然后,清空所有的"微任务(microtask)"队列(如 Promise.thenqueueMicrotaskawait 之后的代码);③ 接着,从"宏任务(macrotask)"队列里,一个宏任务执行(如 setTimeoutsetInterval、IO 回调);④ 执行完这一个宏任务后,再去清空一遍微任务队列;⑤ 然后再取下一个宏任务……如此循环关键就在于:微任务的优先级,高于宏任务——每执行完一个宏任务(以及最初的同步代码),都会把当前所有的微任务清空,然后才去取下一个宏任务。所以,我那个 Promise.then(微任务),会排在 setTimeout(宏任务)前面执行——哪怕 setTimeout 写在前面、延迟是 0!因为 setTimeout(fn, 0) 也只是把 fn 扔进了"宏任务队列",而它必须等所有微任务都清空了,才会被取出来执行我这才痛彻地明白:在 JavaScript 里,异步代码的执行顺序,不由"书写顺序"决定,而由"事件循环的调度规则(同步 → 微任务 → 宏任务)"决定;setTimeout(fn, 0) 不是"立即执行",而是"下一个宏任务时机,且要排在所有微任务之后"执行不理解事件循环,就会对异步代码的执行顺序,产生各种"反直觉"的误判;而要写对、调对异步代码,就必须在脑子里,建立起"同步 / 微任务 / 宏任务"这个三层执行模型;需要严格顺序时,不能靠"把它写在前面",而要用 await / then 把它们显式地串联起来

故障现场:打印顺序完全不按书写顺序

我把这个"顺序反直觉"的现场,摊开给你看:

// ✗ 困惑: 打印顺序完全不按代码书写顺序
console.log("1 同步");

setTimeout(() => {
    console.log("2 setTimeout (宏任务)");
}, 0);                                  // 写在前面, 延迟 0

Promise.resolve().then(() => {
    console.log("3 Promise.then (微任务)");
});                                     // 写在后面

console.log("4 同步");

// 你以为的顺序(按书写): 1, 2, 3, 4
// 实际打印顺序:
//   1 同步
//   4 同步                  ← 同步代码先全跑完
//   3 Promise.then (微任务)  ← 微任务优先于宏任务!(虽然写在后面)
//   2 setTimeout (宏任务)    ← 宏任务最后(虽然写在前面、延迟0)

// 为什么? 事件循环的执行规则:
//   1. 先同步执行所有同步代码 → 打印 "1", "4"。
//      (setTimeout 和 Promise.then 只是"注册"了回调, 没立刻执行)
//   2. 同步代码跑完 → 清空所有"微任务" → 执行 Promise.then → 打印 "3"。
//   3. 微任务清空了 → 取一个"宏任务" → 执行 setTimeout → 打印 "2"。

// 关键: 微任务优先级 > 宏任务
//   - 每次同步代码 / 一个宏任务执行完, 都会"把微任务队列清空", 才取下一个宏任务。
//   - setTimeout(fn, 0) ≠ 立即执行, 而是"下一个宏任务时机 + 排在所有微任务后"。

// 更绕的例子(微任务里又产生微任务):
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => {
    console.log("C");
    Promise.resolve().then(() => console.log("D"));  // 微任务里又加微任务
});
console.log("E");
// 顺序: A, E, C, D, B
//   (同步 A,E → 微任务 C, C里又加的 D 也在本轮微任务清空时执行 → 最后宏任务 B)

// 根因: JS 单线程事件循环, 执行顺序是 同步→清空微任务→取一个宏任务→再清空微任务,
//   微任务优先于宏任务; 不是按书写顺序, setTimeout(0) 也要排在微任务之后。

看着这"1, 4, 3, 2"的实际顺序,我才算彻底想明白了根源。问题的核心,是 JS 事件循环的执行规则不是按书写顺序:① 先同步执行所有同步代码(打印 1、4——setTimeoutPromise.then 只是"注册"了回调、没立刻执行);② 同步跑完,清空所有微任务(执行 Promise.then,打印 3);③ 微任务清空了,取一个宏任务(执行 setTimeout,打印 2)关键是:微任务优先级 > 宏任务——每次同步代码/一个宏任务执行完,都会"把微任务队列清空"才取下一个宏任务;所以 setTimeout(fn, 0) ≠ 立即执行,而是"下一个宏任务时机 + 排在所有微任务之后"更绕的例子里,微任务里又产生的微任务(D),也会在本轮微任务清空时一起执行,所以顺序是 A、E、C、D、B。归根结底:JS 单线程事件循环,执行顺序是"同步 → 清空微任务 → 取一个宏任务 → 再清空微任务",微任务优先于宏任务;不是按书写顺序,setTimeout(0) 也要排在微任务之后——这,就是根源。

第一件事:搞懂事件循环的三层执行模型

定位到根源,我必须把"事件循环、同步/微任务/宏任务"从根上彻底搞清楚:

JS 事件循环: 同步 → 清空微任务 → 取一个宏任务 → 再清空微任务 ...

# 大前提: JS 是单线程
#   - 同一时刻只能做一件事; 异步靠"事件循环"调度回调, 不是多线程并行。

# 三类任务:
#   1. 同步代码: 当前正在执行的代码, 直接在调用栈上跑。
#   2. 微任务(microtask): 优先级高。
#      - Promise.then/catch/finally、queueMicrotask、await 后面的代码、MutationObserver。
#   3. 宏任务(macrotask): 优先级低。
#      - setTimeout、setInterval、setImmediate、I/O、UI 渲染、事件回调。

# 执行循环(关键规则):
#   1. 执行所有同步代码(把调用栈跑空)。
#   2. 清空"整个微任务队列"(执行期间新加的微任务也一并执行, 直到空)。
#   3. 取"一个"宏任务执行。
#   4. 回到第2步: 再清空整个微任务队列。
#   5. 再取下一个宏任务 ... 循环往复。
#   → 核心: 微任务"成批清空", 宏任务"一次取一个", 微任务永远先于下一个宏任务。

# 所以这些"反直觉":
#   - Promise.then 比 setTimeout(0) 先执行(微任务 > 宏任务)。
#   - setTimeout(fn, 0) 不是立即, 而是"下个宏任务且在微任务之后"。
#   - 大量微任务会"饿死"宏任务(微任务没清空就不取宏任务)。
#   - await 后面的代码 = 微任务(相当于 .then 里的)。

# 实用判断:
#   - 想知道顺序: 先同步, 再所有微任务, 再一个宏任务, 再所有微任务 ...
#   - 需要严格顺序: 别靠"写在前面", 用 await/then 显式串联。

# 关键认知: 异步顺序由事件循环规则决定, 不由书写顺序决定。

# 核心: JS 单线程事件循环, 同步→清空微任务→取一个宏任务→再清空微任务;
#   微任务(Promise.then/await)优先于宏任务(setTimeout); 顺序看规则不看书写位置。

原理终于清晰了。大前提:JS 是单线程,同一时刻只能做一件事,异步靠"事件循环"调度回调、不是多线程并行三类任务:① 同步代码(直接在调用栈跑);② 微任务(优先级高:Promise.then/queueMicrotask/await 后的代码);③ 宏任务(优先级低:setTimeout/setInterval/IO/UI 渲染/事件回调)执行循环的关键规则:① 执行所有同步代码;② 清空"整个微任务队列"(执行期间新加的微任务也一并执行直到空);③ 取"一个"宏任务执行;④ 回到第 2 步再清空微任务;⑤ 再取下一个宏任务……——核心:微任务"成批清空",宏任务"一次取一个",微任务永远先于下一个宏任务所以那些反直觉:Promise.thensetTimeout(0) 先执行、setTimeout(0) 不是立即、大量微任务会"饿死"宏任务、await 后面的代码就是微任务由此,我刻下一个关键认知:异步顺序由事件循环规则决定,不由书写顺序决定;想知道顺序就按"先同步、再所有微任务、再一个宏任务、再所有微任务"推;需要严格顺序就用 await/then 显式串联。归根结底:JS 单线程事件循环,同步→清空微任务→取一个宏任务→再清空微任务;微任务(Promise.then/await)优先于宏任务(setTimeout);顺序看规则不看书写位置。

第二件事:正解——需要顺序就用 await/then 显式串联

搞懂了原理,正解就清晰了:别靠"写在前面"来保证顺序,需要严格顺序就用 await/then 把异步操作显式串联起来

// ✓ 正解一: 需要"A 完成后再 B", 用 await 显式串联
async function run() {
    const a = await stepA();   // ✓ 等 A 完成
    const b = await stepB(a);  // ✓ 再做 B(用 A 的结果)
    const c = await stepC(b);  // ✓ 再做 C
    return c;
    // → await 让异步代码"看起来像同步", 顺序明确, 不用猜事件循环。
}

// ✓ 正解二: 用 .then 链式串联(等价, 不用 async 时)
stepA()
    .then(a => stepB(a))
    .then(b => stepC(b))
    .catch(err => handle(err));   // ✓ 别忘了 catch

// ✓ 正解三: 多个"互相独立"的异步, 想并发就 Promise.all(别串行等)
const [r1, r2, r3] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// → 三个同时发起, 一起等; 比 await 一个个串行快得多(它们互不依赖)。

// ✓ 正解四: 理解 await 就是微任务(别被它"暂停"骗了)
async function f() {
    console.log("1");
    await something();        // await 之后的代码 = 微任务(相当于 .then 里)
    console.log("2");         // 这行是微任务里执行的, 不是同步的!
}
console.log("0"); f(); console.log("3");
// 顺序: 0, 1, 3, 2  (f里 await 前的"1"是同步, await 后的"2"是微任务)

// ✓ 正解五: 别在循环里 await 串行(本可并发的)—— 见 forEach/性能篇
// ✗ for (const x of arr) await fetch(x);  // 串行, 慢
// ✓ await Promise.all(arr.map(x => fetch(x)));  // 并发

// 排查顺序问题: 把每步标上"同步/微/宏", 按 同步→微→宏 推一遍。

// 核心: 需要顺序用 await/then 显式串联, 别靠书写位置; 独立的并发用 Promise.all;
//   记住 await 后是微任务; 别在循环里串行 await。

修复的方向,是"用 await/then 把顺序显式地写出来",而不是依赖书写位置去赌事件循环。正解一,需要"A 完成后再 B",用 await 显式串联:const a = await stepA(); const b = await stepB(a)——await 让异步代码"看起来像同步",顺序明确、不用猜事件循环正解二,用 .then 链式串联(等价、不用 async 时,别忘了 catch)。正解三,多个"互相独立"的异步,想并发就 Promise.all:三个同时发起、一起等,比串行快得多正解四,理解 await 就是微任务:await 之后的代码是微任务(相当于 .then 里的),所以 0, 1, f(), 3 的顺序是 0, 1, 3, 2(await 前的"1"是同步、await 后的"2"是微任务)。正解五,别在循环里 await 串行(本可并发的用 Promise.all(arr.map(...)))。归根结底:需要顺序用 await/then 显式串联、别靠书写位置;独立的并发用 Promise.all;记住 await 后是微任务;别在循环里串行 await

第三件事:事件循环引发的其他常见现象

这次踩坑后,我把事件循环引发的其他常见现象,也一并梳理清楚了——它能解释很多 JS 的"为什么":

事件循环能解释的其他现象

# 1. setTimeout(fn, 0) 也不是"0 毫秒后立即执行"
#   - 它是"至少 0ms 后, 且要等当前同步代码+所有微任务跑完, 才作为宏任务执行"。
#   - 实际最小延迟还受浏览器限制(嵌套深了有 4ms 下限)。

# 2. 长同步任务会"卡死"页面/服务
#   - 单线程: 一个长循环/重计算占着, 事件循环转不动 → UI 卡死、其他回调饿死。
#   - → 拆分任务(分片 + setTimeout 让出)、或用 Web Worker(浏览器)。

# 3. 大量微任务"饿死"宏任务和渲染
#   - 微任务没清空就不取宏任务、不渲染 → 死循环式 Promise 会卡死页面。
#   - → 别制造无限微任务链。

# 4. await 会"暂停"函数, 但不阻塞线程
#   - await 时, 函数挂起、控制权交还事件循环去做别的; 不是"卡住整个线程"。

# 5. 渲染时机(浏览器)
#   - 通常: 一轮宏任务 + 微任务清空后, 才可能渲染一帧。
#   - 想在渲染前做事用 requestAnimationFrame(它在渲染前的特定时机)。

# 6. Node 和浏览器的事件循环有差异
#   - Node 有 process.nextTick(比 Promise 微任务还优先)、phases 等细节。
#   - 大原则一致, 但细节(尤其 timer/IO 顺序)有别, 别想当然跨环境。

# 关键认知: 理解事件循环, 才能解释 JS 异步的"为什么", 也才能写出不卡顿的代码。

# 核心: 事件循环解释了 setTimeout(0)不立即、长任务卡死、微任务饿死宏任务、
#   await不阻塞线程、渲染时机等; 懂它才能写对异步、不卡 UI。

原来事件循环能解释 JS 异步的这么多"为什么"setTimeout(fn, 0) 不是"0 毫秒立即执行"(而是"至少 0ms 后、且等当前同步+所有微任务跑完才作为宏任务执行",实际还有 4ms 下限);长同步任务会"卡死"页面/服务(单线程,一个长循环占着事件循环转不动,要拆分任务或用 Web Worker);大量微任务"饿死"宏任务和渲染(微任务没清空就不取宏任务、不渲染,无限微任务链会卡死页面);await 会"暂停"函数但不阻塞线程(挂起函数、控制权交还事件循环去做别的);渲染时机(一轮宏任务+微任务清空后才可能渲染一帧,要在渲染前做事用 requestAnimationFrame);Node 和浏览器的事件循环有差异(Node 有 process.nextTick 等细节、别想当然跨环境)。它给我的启发是:事件循环,是理解 JS 异步行为的"总钥匙"——几乎所有 JS 异步的"反直觉",都能用它解释清楚;懂它,才能写对异步、不卡 UI、不被诡异的执行顺序坑到归根结底:事件循环解释了 setTimeout(0) 不立即、长任务卡死、微任务饿死宏任务、await 不阻塞线程、渲染时机等;懂它才能写对异步、不卡 UI。

下面这张图,是这次"顺序反直觉"的成因与执行模型:

第四件事:微任务 vs 宏任务速查对照

这次踩坑后,我把常见的微任务、宏任务,以及它们的执行特点,整理成一张速查表。

类型 典型 API 优先级 执行特点
同步代码 普通语句 最高(最先) 当前栈直接跑完
微任务 Promise.then/catch/finally、await 后、queueMicrotask 成批清空(本轮全跑完)
宏任务 setTimeout、setInterval、I/O、事件回调 一次只取一个
(Node)nextTick process.nextTick 比微任务还高 Node 专有
渲染(浏览器) requestAnimationFrame 在宏任务间、渲染前 每帧前执行

这张表,把"哪些是微任务、哪些是宏任务"理清了。记住两组对照:微任务(Promise.thenawait 后、queueMicrotask)优先级高、成批清空(本轮全跑完);宏任务(setTimeoutsetInterval、IO、事件回调)优先级低、一次只取一个关键的执行特点差异是:微任务"成批"——一旦开始清,就把当前队列(含期间新加的)全清空;宏任务"一个一个"——取一个、执行、再去清微任务、再取下一个(此外 Node 还有比微任务更优先的 process.nextTick,浏览器有渲染前执行的 requestAnimationFrame。)它给我的启发是:判断异步执行顺序,关键就是分清每个异步操作"是微任务还是宏任务",再套用"同步 → 清空微任务 → 一个宏任务 → 再清空微任务"的规则;记住这张表里"谁是微、谁是宏",你就能推算出任何一段混合异步代码的执行顺序,再也不会被"Promise 怎么比 setTimeout 先跑"这种问题困住

第五件事:面试常考、实战常坑的事件循环题

事件循环既是面试常考,也是实战常坑。我把几类典型的"顺序题"和它们的规律梳理了一下。

场景 关键规律
setTimeout vs Promise.then Promise(微)先于 setTimeout(宏)
多个 Promise.then 链 同一轮微任务里, 按加入顺序依次执行
async/await 拆解 await 前是同步, await 后是微任务
微任务里再加微任务 本轮一起清空(直到队列空)
宏任务里加微任务 该宏任务执行后, 立刻清空这些微任务
嵌套 setTimeout 每个是独立宏任务, 一轮取一个

这张表,把事件循环"顺序题"的规律提炼了出来。核心还是那套规则的应用:Promise(微)永远先于 setTimeout(宏);async/await 要拆成"await 前的同步"和"await 后的微任务"两段;微任务里再加的微任务本轮一起清空;宏任务里加的微任务在该宏任务后立刻清空;嵌套的 setTimeout 是一个个独立宏任务、一轮取一个它给我的启发是:事件循环之所以是面试的"高频考点"和实战的"高频坑点",正因为它是JS 异步行为的底层运行机制——懂了它,你不仅能答对那些"请说出打印顺序"的面试题,更能在实战中预判异步代码的行为、写出顺序正确且不卡顿的程序而它考的从来不是"死记某个顺序",而是"你是否真正理解了那套'同步→微→宏'的调度规则,并能用它去推导任意场景"掌握底层规则,胜过背诵一百个例子——这,是事件循环这个主题,给我的最大启示。

第六件事:写异步代码时,我现在会怎么决策

现在,每当我写涉及多个异步操作的代码,脑子里都会过一遍这张决策图——核心就一问:它们之间有顺序依赖吗?我靠什么保证顺序?

这张图的灵魂,是那个必问的问题:这些异步操作之间,有顺序依赖吗?我靠什么保证顺序?第一问:有顺序依赖吗?——没有(互相独立),用 Promise.all 并发、一起等(快);有(A 完成才能做 B),用 await/then 显式串联在循环里再细分:循环里且各项独立,用 Promise.all(arr.map(...)) 并发(别串行 await);循环里且必须顺序,用 for...of + await 串行而贯穿始终的铁律是:绝不靠"写在前面"来保证顺序;真要排查顺序问题,就按"同步 → 微任务 → 宏任务"的规则推一遍这套判断,让我写异步代码时,不再凭"书写顺序"的错觉去赌执行顺序——核心始终是:要顺序就显式串联,要并发就 Promise.all,别靠书写位置。

我立下的几条规矩

这场"顺序反直觉"的事故,换来了我写 JavaScript 时,刻进骨子里的几条铁律:

  1. 异步顺序由事件循环决定,不由书写顺序决定。同步→清空微任务→取一个宏任务→再清空微任务,微任务优先于宏任务。
  2. Promise.then 比 setTimeout(0) 先执行。前者微任务、后者宏任务;setTimeout(0) 不是立即,要排在所有微任务后。
  3. 需要顺序就用 await/then 显式串联。别靠"写在前面"来保证顺序,那是错觉。
  4. 独立的异步用 Promise.all 并发。互不依赖的别串行 await,一起发起、一起等,快得多。
  5. await 后面的代码是微任务。await 前是同步、await 后是微任务,推顺序时要拆开看。
  6. 别用长同步任务/无限微任务卡死事件循环。长任务拆片或用 Worker,别制造无限微任务链。
  7. 推顺序就按"同步→微→宏"套规则。给每步标上类型,一步步推,别死记例子。

附:一道经典事件循环题,逐步推演执行顺序

口说无凭。下面这道融合了同步、Promise、setTimeout、async/await 的经典题,带你一步步推演执行顺序——会推这一道,就掌握了规律:

console.log("1");                                  // 同步

setTimeout(() => console.log("2"), 0);             // 宏任务

async function asyncFn() {
    console.log("3");                              // 同步(调用时立刻执行到 await)
    await Promise.resolve();
    console.log("4");                              // await 后 = 微任务
}
asyncFn();

new Promise((resolve) => {
    console.log("5");                              // 同步! (Promise 构造器是同步执行的)
    resolve();
}).then(() => console.log("6"));                   // 微任务

console.log("7");                                  // 同步

// ===== 逐步推演 =====
// 第1步 跑所有同步代码(从上到下):
//   "1"  (console.log)
//   "3"  (asyncFn() 调用, 执行到 await 前)
//   "5"  (new Promise 的执行器是同步的!)
//   "7"  (最后一行同步)
//   → 同步阶段输出: 1, 3, 5, 7
//   (此时: 微任务队列 = [asyncFn的"4", Promise的"6"]; 宏任务队列 = [setTimeout的"2"])

// 第2步 清空所有微任务(按加入顺序):
//   "4"  (asyncFn await 后的代码)
//   "6"  (.then 回调)
//   → 微任务阶段输出: 4, 6

// 第3步 取一个宏任务:
//   "2"  (setTimeout 回调)

// 最终顺序: 1, 3, 5, 7, 4, 6, 2

// 易错点:
//   - "5" 是同步! new Promise(executor) 的 executor 立即同步执行(只是 then 是异步)。
//   - "3" 是同步! async 函数体在遇到第一个 await 前是同步执行的。
//   - "4"、"6" 是微任务, 在同步后、setTimeout("2") 前执行。

// 核心: 推事件循环题就三步 —— 先列出所有同步输出, 再清空微任务队列,
//   再取宏任务; 注意 Promise构造器和async的await前都是"同步"。

这道题,把推演事件循环的方法,一步步演示了出来推演就三步:第一步,跑所有同步代码——输出 1(直接)、3(asyncFn() 调用、执行到 await 前)、5(new Promise 的执行器是同步执行的!)、7(最后一行),同步阶段:1, 3, 5, 7;第二步,清空所有微任务(按加入顺序)——4(asyncFn await 后)、6(.then 回调),微任务阶段:4, 6;第三步,取一个宏任务——2(setTimeout)。最终:1, 3, 5, 7, 4, 6, 2这里有两个最常见的易错点:5 是同步(new Promise(executor) 的 executor 立即同步执行,只是 .then 才是异步);3 是同步(async 函数体在遇到第一个 await 前,是同步执行的)。这,正是我想用这道题,留给每一个学 JS 的人的最后一课:面对任何"请说出执行顺序"的事件循环题(无论面试还是实战调试),都别凭感觉猜,而是拿出纸笔,严格按"先列同步、再清微任务、再取宏任务"这三步,把每一行归类、推演一遍当你能稳稳地推对这类题时,你对 JS 异步的理解,就从"似懂非懂"真正变成了"了然于胸";而那些曾让你困惑的"反直觉",也会变成"理所当然"

写在最后

回头看,这场由"事件循环"引发的、执行顺序反直觉的事故,真正教给我的,是一个比"记住微任务优先"本身更深的道理:我们写下的代码,它"书写的顺序(空间上的上下)",和它"实际执行的顺序(时间上的先后)",可能是两回事;尤其在异步、并发的世界里,"代码长什么样"和"何时、以什么顺序运行",被一套你必须去理解的"调度机制"隔开了我之前的错误,是用"顺序阅读代码"的线性直觉,去理解一个"由事件循环非线性调度"的异步程序——我以为"我先写的就先跑",却不知道那些异步操作,其实是被"注册"到不同优先级的队列里、由事件循环按规则重新排序执行。这让我深刻地领悟到:真正理解一个程序,不能只"静态地、从上到下地读它的文本",更要能"动态地、在脑海里模拟它的执行"——尤其要理解它背后那个"决定何时执行什么"的运行时机制(对 JS 而言, 就是事件循环)从"阅读代码的文本"到"在脑中运行代码的执行",从"顺序的线性思维"到"调度的异步思维"——这种思维的跃迁,正是从"会写同步代码"走向"驾驭异步编程"的关键一跳。读懂代码"写的样子",更要读懂它"跑的样子"——这,是我用一次"顺序反直觉"的事故,换来的、关于 JavaScript、也关于"如何真正理解程序"的、最朴素也最深刻的领悟。如果这篇复盘,能让你在下一次看到混合异步代码时,能在脑中按"同步→微→宏"推出它的真实顺序,那我对着那个反直觉的打印顺序熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我在 Python 里拷贝了一个嵌套列表,以为是独立副本,结果改副本的内层元素原列表也跟着变了,我对着浅拷贝只复制一层排查了大半天的复盘

2026-6-2 4:59:35

技术教程

我用等号判断 Go 的错误类型一直好好的,下游一改成用 %w 包装错误,我的判断就全失效、走错了分支,我对着错误链排查了大半天的复盘

2026-6-2 5:13:59

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索