JavaScript是单线程语言,因DOM操作安全性和执行模型简洁性而设计为仅有一个调用栈;事件循环严格遵循“清空微任务→可能渲染→执行一个宏任务”流程,微任务总在宏任务之间插队执行。

JavaScript 是单线程语言,不是“可以改成多线程”,而是设计上就拒绝多线程——DOM 操作安全性和执行模型简洁性决定了它必须如此。
为什么浏览器不允许 JS 多线程?
这不是技术做不到,而是故意不让你做。比如两个线程同时操作 document.body:一个在 appendChild,另一个在 removeChild,浏览器根本没法决定谁该赢。这种竞态条件会直接导致 UI 行为不可预测。所以从诞生起,JS 就只给一个调用栈(Call Stack),所有同步代码排队执行,这是铁律。
事件循环不是“轮询”,而是“清空微任务 + 执行一个宏任务”的固定节奏
很多人误以为事件循环是“不断检查队列有没有新任务”,其实它有严格流程:
- 同步代码执行完,调用栈变空 → 立刻执行所有微任务(
Promise.then、queueMicrotask、async/await后续) - 微任务队列彻底清空(包括执行过程中新产生的微任务)→ 浏览器可能触发一次 UI 渲染(如 DOM 更新可见)
- 再从宏任务队列取一个任务执行(
setTimeout、setInterval、script全局代码、I/O 回调等)
关键点:setTimeout(fn, 0) 不是“立刻执行”,而是“当前宏任务结束后、下一轮宏任务开始时执行”;而 Promise.resolve().then(fn) 是“当前宏任务结束前,一定执行完”。
立即学习“Java免费学习笔记(深入)”;
常见踩坑:你以为的“先后”,其实是微/宏任务错位
下面这段代码输出顺序常被猜错:
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
结果是 1 → 4 → 3 → 2。原因不是“Promise 更快”,而是:setTimeout 进宏任务队列,Promise.then 进微任务队列,而微任务总在宏任务之间插队执行。
容易忽略的细节:
-
async函数内部await后的代码,本质是微任务(等价于Promise.then) -
requestAnimationFrame是宏任务,但浏览器会在下次重绘前执行,优先级介于微任务和普通宏任务之间 - Node.js 中
process.nextTick比微任务还高,会在本轮宏任务结束、微任务开始前执行(仅限 Node)
真正难的不是记住顺序,而是写出异步逻辑时不依赖“肉眼计时”,而是主动用 queueMicrotask 或包装成 Promise 控制时机——因为渲染、用户交互、定时器都跑在同一套事件循环里,差一层微任务,就可能差一帧画面。











