前言
本系列从头手把手带你学习react源码。但是在深入React架构前,你必须先了解一个关键概念,Event Loop(事件循环)模型。
为什么要关心 Event Loop?因为 React 的 Fiber 架构正是基于它的"分片执行"能力构建的。
- 把庞大的更新任务拆成小单元(Fiber 节点)。
- 在每个宏任务间隙(比如一次渲染后),通过 requestIdleCallback 或 scheduler 插入高优先级微任务或宏任务。
- 如果用户有交互(比如点击),浏览器会优先处理 UI 宏任务,React 主动"让出主线程",实现可中断、可恢复、高响应的并发更新。
换句话说:没有 Event Loop,就没有 Concurrent Mode(并发模式) 。这里说的并发其实是 "可中断的渲染(Interruptible Rendering) " + "时间切片(Time Slicing)" 。它是一种 逻辑上的并发,之前有个面试官和我犟过这个事情,所以特地的说明一下。
如果你已经非常熟悉事件循环机制,可以直接跳过本章------因为这里不涉及任何 React 源码。 但如果你只是听说过这个概念,或者脑子里还是一堆碎片化的理解,那这篇文章就是为你准备的。 不管是为了面试,还是为了真正搞懂 React 的底层原理,学完这篇,你都能把这些碎片拼成一张完整的拼图。对了,你还需要了解一个前置概念:Promise。 别担心,只要你用 fetch 或 axios 调用过接口,就一定接触过 Promise。 我们只需要知道它的基本机制就够了。本文主要用它来演示微任务队列的行为,因为没有比 Promise 更直观的微任务示例了。
开始
一开始我听到这个东西的时候,我马上百度了一下啥是事件循环?

以上就是我百度到的结果,"麻了",一句人话都没有!一气之下,我直接关掉了浏览器。 但我冷静了一会儿,认真地思考了一下:不对!不能关浏览器啊。关了它,我还怎么学事件循环? 说到这里,你可能会问:事件循环和浏览器到底有什么关系?它不就是一个抽象的概念吗? 其实不然,事件循环并不是抽象的,而是一个实实在在由浏览器提供的机制。
众所周知,Javascript代码通常 是执行在浏览器端的(我说的是通常),我们只讨论浏览器的执行环境。言归正传,你需要在脑海里有3个概念,主线程 、宏任务队列(Macro Task Queue) 和 微任务队列(Microtask Queue)

主线程
比方:登机口的检票员,就是拿着那个扫码枪扫码的那个人,只有一个检票员的情况
主线程,就是浏览器里唯一执行 JavaScript 代码的线程。JavaScript 的执行是自上而下的。当遇到异步操作时,引擎不会阻塞等待,而是将对应的回调函数放入宏任务队列或微任务队列中,继续执行后续的同步代码。等到当前主线程的任务执行完毕,就会在事件循环里面取出对应的任务,然后在主线程中继续执行。
宏队列(Macro Task Queue)
比方:经济舱检票口的队列
主线程用来存放异步回调函数 的第一个待办清单,主线程干完手头的事后,就从这个队列拿最前面的一个任务出来执行,执行完再拿下一个,一次只拿一个,按顺序来。是的,就这么简单。
常见的宏任务:setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染等
我看到很多文章中武断地认为:"现在没有宏队列这个概念了。" 在我看来,这是前端圈的一个"误解"。他们看到最新的 WHATWG HTML 标准 中没有 "macro task" 这一术语,就以为这个概念被废弃了。 但事实是:规范中从来就没有正式定义过 "macro task"。"宏任务" 是社区为了与"微任务"(microtask)对比而约定俗成的说法。 只不过,现代规范对异步任务的分类和调度机制有了更精细的描述。 在广义的 JavaScript 异步模型中,"宏任务"这一说法依然广泛使用; 而在 HTML 标准中,浏览器通过多个特定类型的任务队列(如延时队列、用户交互队列等)来管理这些任务,从而更精细地控制优先级,保障用户交互的流畅性。
微队列(Microtask Queue)
比方:头等舱检票口的队列,坐过飞机的都知道,只要头等舱队伍有一个人在排队,那么检票员就会把当前经济舱正在检的票检完。然后立刻来头等舱队伍检票,然后直到把这个队伍清空,才会回到经济舱队列。
主线程用来存放异步回调函数 的第二个待办清单 ,只不过,这个队列是个VIP队列,有些特权。当主线执行完宏队列里面当前的宏任务的时候,就程立刻、马上、一口气把微队列里所有的任务全执行完毕。说白了就是能插队。
常见的微任务:Promise.then/catch/finally、queueMicrotask、MutationObserver(浏览器)、process.nextTick(Node,优先级更高)
开始执行JS脚本
这个概念我看了很多的文章,没有一个能解释的通俗易懂,各种术语,概念。在我看来,我们写的JS代码,比如:
js
console.log("hello word")
console.log("hello word1")
console.log("hello word2")
这个代码就是第一个脚本代码,就是我们实实在在写在JS文件里面的代码。开程序开始执行的时候,就把这3行代码,放在了宏队列中,然后这三行代码就是第一个宏任务,所以总结一下三点!
- 程序启动 = 执行第一个宏任务
- 微任务只能是"执行过程中产生的副产品"
- 所以,永远不可能一上来就跑微任务
可视化这两个队列
通过代码的方式直观的感受 -> MicroTask | MacroTask
1.执行脚本中只写了宏任务注册
js
console.log(主线开始)
// 宏任务 1 (积压在后面)
setTimeout(() => console.log('宏1执行'), 0);
// 宏任务 2 (积压在后面)
setTimeout(() => console('宏2执行'), 0);
console.log(主线结束)

2.执行脚本中只写了微任务注册
此时只有微队列有任务Micro=[微1,微2]
js
console.log(主线开始)
// 微任务 1 (积压在后面)
Promise.resolve().then(() => {console.log('微1执行')})
// 微任务 2 (积压在后面)
Promise.resolve().then(() => {console.log('微2执行')})
console.log(主线结束)

3.执行脚本中写了宏任务注册同时写写了微任务注册
此时两个队列现在都有了任务Micro=[微1] / Macro=[宏1]
js
console.log(主线开始)
// 宏任务 1
setTimeout(() => console.log('宏1'), 0);
// 微任务 1
Promise.resolve().then(() => {console.log('微1执行')})
console.log(主线结束)

4.执行脚本中写了宏任务但其中包含了微任务
此时只有一个宏队列有了任务Macro=[宏1]
js
console.log(主线开始)
// 宏任务 1
setTimeout(() => {
// 微任务 1
Promise.resolve().then(() => console.log('微1执行'))
}, 0);
console.log(主线结束)

5.执行脚本中写了微任务但其中包含了宏任务
此时只有一个微队列有了任务Micro=[微1]
js
console.log(主线开始)
// 宏任务 1
const A = new Promise(r => {
setTimeout(() => {
console.log('宏任务(2s)');
r('success1')
}, 2000)
})
console.log(主线结束)

总结
到目前为止,我们已经把浏览器中的 事件循环(Event Loop)模型 拆解成了三个核心角色:
- 主线程:唯一的 JavaScript 执行通道,像登机口那个孤独但高效的检票员;
- 宏任务队列(Macro Task Queue):普通乘客排队区,任务按顺序一个个执行;
- 微任务队列(Microtask Queue):头等舱 VIP 通道,一旦有任务,主线程必须立刻清空整个队列才能干别的。
现在,当你再看到满屏的 setTimeout 和 Promise.then,是不是脑子里自动浮现出两个队伍在登机口排队的画面了?