一、JS 为什么是单线程?
JS 最初设计用途是操作页面 DOM 。
如果 JS 是多线程,就会出现冲突场景:
线程 A 修改 DOM,线程 B 同时删除同一个 DOM,浏览器不知道以哪个操作为准,会引发混乱。
所以规定:JS 主线程只有一个,同一时间只能做一件事,所有 DOM 操作串行执行,避免 DOM 并发冲突。
补充:单线程 ≠ 浏览器只有一个线程。浏览器还有其他辅助线程:定时器线程、HTTP 请求线程、渲染线程,这些线程是浏览器提供的,不属于 JS 主线程。
二、什么是事件循环(Event Loop)
JS 单线程遇到异步任务的时候,不会阻塞等着,而是把异步任务交给浏览器其他线程去处理。
等异步任务满足触发条件后,回调函数会被放到对应的任务队列排队。
事件循环,就是主线程不断读取任务队列、取出任务执行的循环调度机制。
简单一句话:主线程干完手里同步代码,就去队列拿任务执行,周而复始,这个循环就是事件循环。
三、任务分类:宏任务、微任务
✅ 微任务(microtask)
优先级更高,在当前同步代码结束后立刻清空
常见:
- Promise.then / Promise.catch / Promise.finally
- MutationObserver(监听 DOM 变化)
- queueMicrotask()
特点:微任务队列是一个先进先出队列,只要队列里还有微任务,就会一直执行完,才会往下走。
✅ 宏任务(macrotask)
优先级低于微任务
常见:
- setTimeout、setInterval
- AJAX 请求回调
- script 标签整体代码(初始同步代码本身也是宏任务)
- UI 渲染
特点:一次循环只拿一个宏任务执行,执行完之后,再去清空所有微任务。
四、核心执行顺序(重中之重)
- 先执行同步代码(主线程栈里面的代码)
- 同步栈清空 → 一次性清空全部微任务队列(所有微任务依次执行,如果微任务里面又产生新微任务,新微任务继续在本轮清空)
- 微任务队列为空 → 取一个宏任务执行
- 这个宏任务执行完毕后,回到第二步,再次清空微任务,循环往复
口诀:同步优先 → 清光微任务 → 一个宏任务 → 循环
执行流程图文字版:
同步代码栈 → 微任务队列(全部执行完)→ 取 1 个宏任务 → 微任务队列(全部执行完)→ 取下一个宏任务......
五、代码示例
console.log('同步1');
setTimeout(()=>{
console.log('宏任务 setTimeout')
},0)
Promise.resolve().then(()=>{
console.log('微任务 promise')
})
console.log('同步2');
输出顺序:
同步1
同步2
微任务 promise
宏任务 setTimeout
执行拆解:
- 打印同步 1
- setTimeout 交给定时器线程,回调放进宏队列
- Promise.then 回调放进微队列
- 打印同步 2,同步栈清空
- 清空微队列:打印 promise
- 微任务空了,取出宏任务:打印 setTimeout
六、容易踩坑的两点
- 在微任务执行过程中新增的微任务,依旧属于本轮微任务,会接着执行,不会丢到下一轮
- setTimeout 延时 0ms,不是立刻执行,只是尽快加入宏任务队列,必须等本轮微任务全部跑完才执行
总结
- JS 单线程:为了防止多个线程同时操作 DOM 产生冲突;
- 事件循环:JS 主线程调度异步回调的运行机制;
- 顺序规则:同步代码 > 全部微任务 > 单个宏任务,不断循环。
结尾
事件循环是 JS 异步的根基,async/await 本质也是 Promise 语法糖,依旧遵循这套微宏任务规则。弄懂这个,面试异步题、排查异步顺序 bug 就稳很多。