JavaScript非阻塞靠事件循环调度宏/微任务,非多线程;宏任务每轮执行一个并留空隙供渲染与交互,微任务在宏任务后立即清空,确保原子性;长任务需分片或移至Worker。

JavaScript 的非阻塞特性不是靠“多线程”实现的,而是靠事件循环对宏任务和微任务的有序调度。关键不在于“避免执行”,而在于“把耗时操作移出主线程执行流,并让回调在合适时机插入”。
宏任务是事件循环的基本调度单位
每次事件循环只取一个宏任务执行,比如:全局脚本、setTimeout 回调、setInterval 回调、I/O 完成回调、UI 渲染。执行完这个宏任务后,主线程就空闲了——此时页面不会卡死,用户仍可点击、滚动、输入。
- 宏任务之间天然隔离,前一个执行完,后一个才开始,中间留出空隙供渲染或响应用户操作
- 像 setTimeout(fn, 0) 并不是“立刻执行”,而是把 fn 推入宏任务队列,等当前所有同步代码 + 所有微任务跑完后,下一轮循环才执行它
- 浏览器会在某些宏任务后强制触发 UI 渲染(如处理完一个用户点击事件后),保证界面及时更新
微任务提供高优先级、紧耦合的响应能力
每个宏任务结束后,引擎会立即清空整个微任务队列,不穿插任何渲染或新宏任务。这使得 Promise 链、状态更新、错误捕获等逻辑能以原子方式完成,避免中间态暴露给用户。
- Promise.then 回调总在当前同步任务之后、下一个宏任务之前执行,适合做链式数据转换或错误兜底
- queueMicrotask 是显式插入微任务的 API,比 Promise 更轻量,适合需要精确控制执行时机但又不想走 Promise 构造开销的场景
- 微任务不会导致渲染延迟,所以用它批量更新状态(如 Vue/React 的 nextTick)比 setTimeout 更高效
非阻塞算法的核心设计模式
真正让算法“不卡页面”的,不是把它写成异步,而是拆解它的执行节奏,把长耗时操作分片,交由事件循环分批调度。
立即学习“Java免费学习笔记(深入)”;
- 用 setTimeout 或 requestIdleCallback 把大循环拆成多个宏任务,每轮只处理一部分数据,留出时间给 UI 响应
- 用 Promise.resolve().then 在当前任务末尾安排后续步骤,避免同步阻塞,同时保持上下文连续性
- 对计算密集型任务,可结合 Web Worker 移出主线程;Worker 内部虽也是单线程,但它不参与页面渲染,不会影响交互流畅度
- 避免在宏任务中做大量同步计算,比如 for 循环十万次;应改用递归 + setTimeout 或 await queueMicrotask(0) 让出控制权
一个典型非阻塞排序示例
比如对 10 万条数据排序,直接 Array.sort() 可能卡顿几百毫秒。改成:
- 先用 Web Worker 做纯计算(推荐)
- 或在主线程中分块:每次只 sort 1000 条,然后 await queueMicrotask(() => {}) 或 setTimeout(() => {}, 0) 跳到下一轮事件循环
- 过程中可随时响应 cancel 请求,或更新进度条——因为主线程没被锁死


















