☰
事件循环:从浏览器到 Node.js,彻底理解宏任务与微任务的执行顺序
2026/10/6 4:11:57 网站建设 项目流程

1. 为什么说事件循环是 JS 和 Node 的“心脏”

1.1 从“单线程”这个原罪说起

很多刚接触 JavaScript 的人都会有一个困惑:明明 JS 只有一个线程,为什么还能一边处理点击事件、一边发网络请求、一边跑动画?答案就是事件循环(Event Loop)。这个机制其实没那么玄乎,我习惯把它理解成“一个排队叫号的调度员”——它决定了代码里那么多任务到底谁先执行、谁后执行、谁要等着。

先说个最常见的场景。你用浏览器打开一个页面,页面上同时要跑一段耗时很长的 for 循环,还要监听用户点击。如果 JS 真的只有一个线程,而且所有任务都挨个排队执行,那么 for 循环没跑完,点击事件就永远得不到响应,页面会直接卡死。事件循环存在的意义,就是让“会阻塞的代码”靠边站,让“通知类、回调类”的代码插队进来,让整个程序看起来像同时干了很多事。

到了 Node.js 这边,这套机制被进一步放大了。Node 的文件读取、网络请求、数据库操作全都是异步的,底层通过 libuv 实现事件循环调度。你写的fs.readFile并不是真的“读完了再继续”,而是“告诉系统我要读文件,等读完了再叫我”。这个“等完了再叫我”的过程,就是事件循环在背后运作。

1.2 谁需要把这件事搞明白

我可以直接说,以下三类人必须把事件循环彻底吃透:

  • 前端开发者:你在浏览器里写setTimeout、Promise、async/await,如果不知道执行顺序,早晚会被诡异的 Bug 坑哭。
  • Node.js 后端开发者:你写接口、写定时任务、处理消息队列,如果不懂事件循环,根本没法解释“为什么定时器不准”“为什么高并发下内存暴涨”“为什么回调地狱能写成那样”。
  • 面试者:事件循环是前端和 Node 方向的高频考点,从初级到高级都可能被问到。网上相关的面试题一抓一大把,但能把原理讲清楚、还能结合代码分析顺序的人其实不多。

这篇文章我不会只给结论,我会从原理到代码、从浏览器到 Node,把整套事件循环机制拆开讲清楚。你会看到调用栈(Call Stack)、任务队列(Task Queue)、微任务(Microtask)、宏任务(Macrotask)、process.nextTick、setImmediate这些概念到底是怎么串起来的。


2. 事件循环的最小模型:调用栈 + 任务队列

2.1 调用栈到底在干什么

先建立一个最基础的模型。JS 引擎在运行代码时,维护了一个调用栈(Call Stack),这个栈的特点是“后进先出”。函数 A 调用了函数 B,B 调用了函数 C,那么栈里从下到上就是 A、B、C,C 执行完弹出,再执行 B,最后 A。

这个栈里装的都是“正在执行的函数”,它是同步的世界。问题来了:如果某个函数里有个耗时的操作,比如一个三秒的循环,那调用栈就会被它一直占住,期间什么都不能干。这就是为什么“同步代码阻塞”会成为前端性能的头号敌人。

事件循环的第一个核心动作,就是“调用栈为空时,从任务队列里取出下一个任务执行”。这句话你记牢了,后面所有复杂的规则都是围绕它展开的。

2.2 两种任务:宏任务和微任务

你肯定听说过这两个词:宏任务(Macrotask)和微任务(Microtask)。它们之间的区别,直接决定了几行代码的执行顺序。

我用生活场景打个比方。你把工作分成两种:一种是“正常排队办业务的人”,一种是“有 VIP 通道的人”。宏任务就是普通排队的人,微任务就是走 VIP 通道的人。每一个宏任务执行完之后,事件循环不会立刻去拿下一个宏任务,而是先把当前这个宏任务产生出来的“所有微任务”全部清空,然后再进入下一个宏任务。

浏览器里常见的宏任务有:setTimeout、setInterval、I/O 事件、UI 渲染、MessageChannel。常见的微任务有:Promise.then、MutationObserver、queueMicrotask、async/await中 await 之后的代码。

实际看几个代码,你就彻底明白了。

console.log('1'); setTimeout(() => { console.log('2'); }, 0); Promise.resolve().then(() => { console.log('3'); }); console.log('4');

这个输出顺序是1 4 3 2。过程拆一下:同步代码先执行,console.log('1')输出;setTimeout注册一个宏任务,扔进宏任务队列;Promise.then注册一个微任务,扔进微任务队列;console.log('4')输出。同步代码跑完,调用栈空了,先清空微任务,输出3,然后再从宏任务队列里拿出setTimeout的回调,输出2。

这个例子几乎是面试题里的“hello world”,但很多人只是背答案,不知道背后的逻辑。你现在看懂了,后面遇到更复杂的嵌套题目也能自己推。

2.3 为什么微任务要“插队”优先执行

我再说深一层:为什么微任务要优先于宏任务?这其实是设计上的有意为之。微任务的设计初衷,是为了处理“同一个宏任务内部产生的连续性操作”。比如一个 Promise 链式调用,then里又返回一个 Promise,再then。如果微任务不插队,而是被排到宏任务队列后面,那整个 Promise 链的效率会大打折扣,而且用户感知到的延迟会变大。

你再想想,await的本质是什么?await的后面那部分代码,在编译层面就是被包进一个微任务里的。这也是为什么很多人用async/await写代码时,总觉得“代码看起来是同步的,实际上它是异步的”——你看得见的地方是同步写法,看不见的地方全是微任务调度。


3. 浏览器事件循环:比你想的更现实

3.1 渲染、事件、定时器如何共存

浏览器的环境比 Node 复杂,因为浏览器除了要执行 JS,还要负责渲染页面、处理用户输入、跑动画。这意味着事件循环不仅要管 JS 代码,还要和“渲染更新”协调。

标准模型是这样的:每执行完一个宏任务,浏览器会检查是否需要重新渲染。如果需要,就在进入下一个宏任务之前渲染一次。这个“渲染时机”是微任务清空之后、下一个宏任务开始之前。所以你会发现,如果你在一个微任务里修改了 DOM,可能不会立刻触发渲染,而是要等多个微任务跑完才渲染。

这里有一个非常经典的坑:用setTimeout实现“等渲染完成后再执行”。比如你想要一个元素从 A 状态变成 B 状态后,再读取它的尺寸。如果你用同步代码改完样式立刻读,读到的是旧值;如果你用setTimeout(..., 0),因为宏任务执行完会先渲染,所以你读到的就是新值。这个技巧在老项目里很常用,现在也可以用requestAnimationFrame或者双 rAF来代替,但原理是一样的。

3.2 别再被“setTimeout 等于准时执行”骗了

我必须强调一件事:setTimeout的第二个参数只是“最小延迟时间”,不是“保证执行时间”。调用栈里有大量同步代码在跑时,定时器的回调到点了也只能等着。比如你写一个setTimeout(fn, 0),但前面有一段 2 秒的同步循环,那fn一定在 2 秒后才执行,而不是“立刻”。

这个特性在生产环境里特别容易坑人。举个例子,你做图片懒加载,用一个轮询去检测图片是否进入视口。你本来想着每 100ms 检查一次,结果某个时间里主线程被一段重计算阻塞了 1 秒,那这 1 秒内定时器全部堆积,等主线程腾出来,连续跑好几个定时器回调,表现就是“一段时间没反应,然后突然爆发执行”。真正需要平滑的东西,我不会依赖setTimeout,宁可把它换成requestAnimationFrame加上时间差判断。

再补充一个细节:HTML 规范里对嵌套的setTimeout有最少 4ms 的限制。意思是,当定时器回调里又创建一个定时器,并且嵌套层级超过 5 层,浏览器会强制把延迟时间下限提到 4ms。这个限制不是为了坑你,而是为了保护 CPU 和电池,防止页面因为疯狂的定时器而耗尽资源。所以你在写游戏循环或者高频轮询时,setTimeout并不是一个精准的工具。

3.3 事件循环里的“饿死”风险

微任务虽然插队快,但它有个致命问题:如果微任务队列永远清不空,宏任务就永远没有机会执行。

function loop() { Promise.resolve().then(loop); } loop();

这段代码会不断往微任务队列追加新任务,结果主线程永远在处理微任务,定时器、点击事件全部停在队列里。在浏览器里,这个页面会直接失去响应。这是“饿死”(starvation)现象,写代码的时候要格外注意。我见过一些同事用微任务做“递归遍历大数据”,一不小心就会把一个表格页面卡死在加载动画里,排查半天才发现是微任务递归导致宏任务排队排不上。


4. Node.js 事件循环:六个阶段一个都不能少

4.1 libuv 和它的六个阶段

进入 Node.js,你会发现事件循环变成了一个更加“工业化”的调度器。Node 基于 libuv 这个 C 语言库实现事件循环,整套机制被划分为六个阶段,循环本身就是这六个阶段按顺序转圈跑:

┌───────────────────────────┐ ┌─►│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ poll │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ close callbacks │ │ └───────────────────────────┘ └──────────────────────────────┘

我依次解释一下每个阶段,重点讲 poll 和 check,因为这两个阶段是编码时最容易接触到的。

  • timers 阶段:处理setTimeout和setInterval到期的回调。
  • pending callbacks 阶段:处理系统操作的回调,比如 TCP 错误、文件系统错误等。
  • idle, prepare 阶段:仅供 libuv 内部使用,你写业务代码基本碰不到。
  • poll 阶段:核心阶段。它会获取新的 I/O 事件、处理 I/O 回调,同时也会等待新的 I/O 事件发生。如果没有定时器到期,poll 会一直阻塞等待。
  • check 阶段:处理setImmediate回调。注意,setImmediate不是setTimeout的替代品,它有自己的调度位置。
  • close callbacks 阶段:处理close事件,比如 socket 关闭,process.on('exit')之类的清理回调。

4.2 process.nextTick 是“插队的VIP中的VIP”

Node 里有一个.nextTick机制。先说结论:process.nextTick在技术上并不属于事件循环的任何一个阶段,它是在每个阶段之间被优先执行的。也就是说,任何阶段运行完,接下来都是先处理process.nextTick队列,然后再进入下一个阶段。

这就导致了一个特性:process.nextTick的优先级比Promise.then还要高。

process.nextTick(() => { console.log('nextTick'); }); Promise.resolve().then(() => { console.log('promise'); });

输出顺序是nextTick在前,promise在后。原理是:事件循环每进入一个阶段,做完该阶段的任务后,在处理微任务队列之前,先会把整个process.nextTick队列清空。如果你在process.nextTick回调里又调用了process.nextTick,那事件循环会一直清下去,直到队列空为止。

这里有个风险:无限递归的process.nextTick比微任务递归更容易卡死进程。在 Node 文档里,官方甚至专门警告过,process.nextTick在递归场景下会导致 I/O 操作永远得不到执行。所以我会告诉你一个经验法则:能用setImmediate解决的事情,不要用process.nextTick。process.nextTick适合的场景是“在当前代码执行完之后、事件循环继续之前,必须立即回调”的情况,比如某些内部错误处理、流式接口的断点清理。

4.3 setImmediate 和 setTimeout 到底谁先执行

这是一个让无数人纠结的问题。在 Node 里setImmediate和setTimeout(cb, 0)的顺序,在不同的场景下结果是不同的。

如果你在模块的顶层(即主模块)调用它们:

// 在 main.js 里 setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });

这个结果不固定。可能是timeout先,也可能是immediate先。原因在于:启动进程后,进入事件循环的时机受系统性能影响,timers 阶段执行时定时器是否已经到期并不确定。如果你在 I/O 循环内部调用同样的代码:

const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });

这次结果几乎永远是immediate先输出。因为fs.readFile的回调在 poll 阶段执行,poll 阶段结束后会立即进入 check 阶段,而setImmediate的回调恰恰就在 check 阶段执行;定时器要等到下一次循环的 timers 阶段才轮到。这个“固定顺序”背后就是阶段推进的逻辑。

实际编码中,除非你在做框架或者底层工具,否则没必要纠结这个顺序。但在面试里,这个点经常被拎出来考,所以我把底层逻辑讲明白了,你就不怕被反复追问。

4.4 文件读取到底走了哪条路径

我再用一个实例帮你把整套流程串起来。

const fs = require('fs'); console.log('start'); fs.readFile(__filename, () => { console.log('readFile callback'); }); setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); process.nextTick(() => { console.log('nextTick'); }); Promise.resolve().then(() => { console.log('promise'); }); console.log('end');

输出顺序是:

start end nextTick promise readFile callback immediate timeout

这里有一个很关键的点:readFile的回调出现在immediate和timeout之前。为什么?因为文件读取是异步 I/O,它是在内核层面完成读操作后,在 poll 阶段触发回调。而 poll 阶段在 check 之前、下一次 timers 之前。所以顺序是:poll 阶段执行readFile回调,然后 check 阶段执行setImmediate,最后下一轮循环的 timers 阶段执行setTimeout。

这个例子能帮你把抽象的阶段设计和真实代码对应上,我建议你亲自跑一遍,感受一下输出顺序,再对着阶段图去推。


5. Node 与浏览器:有相似,更有差异

5.1 都是事件循环,差别在哪

浏览器的事件循环以“渲染”为重要节点,Node 的事件循环以“系统 I/O 的调度”为核心。两者最大的差异在于:

  • 浏览器有微任务清空动作,但微任务不是在一个独立阶段,而是每个宏任务之后执行;Node 里process.nextTick优先级最高,Promise 微任务在各阶段之间执行。
  • Node 没有渲染线程,所以不需要考虑 UI 更新的时机。
  • Node 有setImmediate,浏览器没有;浏览器有requestAnimationFrame,Node 没有。
  • 浏览器的宏任务通常被理解为“用户交互事件、定时器、UI 渲染”,Node 的宏任务被明确划分为 phase。

5.2 三个“千万别”的经验总结

我在实际开发里总结过几个容易踩的坑,写在这里:

第一,别在 Node 里用全局Promise递归处理超大列表。很多人图省事,把列表分片后一个个 await,遇到几十万条数据就彻底崩了。原因是微任务队列被塞满,I/O 回调完全没法插入。正确处理是分块配合setImmediate或者用流式处理,让出控制权给事件循环。

第二,别依赖setTimeout做精确调度。做定时任务、延迟重试、限流时,setTimeout的误差在 Node 里也是存在的,而且受 CPU 负载影响很大。如果你需要精确间隔,可以考虑基于时间戳的循环检查,或者直接引入 cron-like 的调度库。

第三,别在nextTick里做递归和重逻辑。一旦不停往nextTick队列里追加,整个进程的 I/O 都会被饿死。Node 依赖异步 I/O 作为核心能力,你把 I/O 饿死等于把服务搞瘫。


6. 实战:为什么你的定时任务总不准、高并发下卡顿

6.1 定时任务不准,根源在“忙等”

很多 Node 后端会在项目里用setInterval写定时任务。比如每 10 秒扫一次数据库里的待处理订单。表面上看,代码很简单:

setInterval(async () => { const orders = await getPendingOrders(); await processOrders(orders); }, 10000);

然而如果processOrders一次处理了特别多订单,耗时超过 10 秒,那么下一次setInterval触发时,上一个执行还没结束。这时就会发生“回调堆积”:事件循环发现定时器到期后,开始执行回调,但回调内部又在做异步等待,导致这段期间内新的定时器回调又被标记为到期,等上一次做完,紧接着又立刻跑下一次。表现就是你明明设置了 10 秒间隔,实际执行可能变成“做完一次紧接着做第二次”,没有休息时间。

解决方案至少有两种:

  • 动态调度:每次执行完再设置下一次定时器,用setTimeout代替setInterval。
  • 节流标志位:用一个布尔变量锁住,防止上一次没结束就启动下一次。

这两种方法我都在生产环境用过,第二种更简单,但第一种更优雅。从事件循环的视角看,它们本质都是避免“调用栈和任务队列之间的恶性堆积”。

6.2 高并发“卡顿”,先查主线程阻塞

你可能见过这种情况:Node 进程 CPU 打满,但很多请求的响应时间飙升到秒级。这时候先别急着调数据库连接数,十有八九是主线程被同步阻塞了。

比如你在一个请求里对一个大数组做了深拷贝、复杂 JSON 解析、或者某种加密计算,这些操作都是同步占用调用栈的。一个请求卡 2 秒,后面所有请求的异步回调全部排队,整个服务就像堵车一样。Node 的异步优势只能体现在“非 CPU 密集型”场景。一旦遇到 CPU 密集型操作,你会发现事件循环的调度能力再强也没用。

这类问题的排查思路很固定:先用--prof或者火焰图工具定位热点函数,然后把这个同步操作拆分成异步分片处理、交给 worker_threads,或者干脆换用其他语言/服务来做。事件的循环本身没有问题,问题是你的同步代码“占着茅坑不拉屎”。

6.3 用它理解“背压”和“流式处理”

再往深一点说,事件循环的核心价值之一,是让“背压”(Backpressure)成为可能。所谓背压,就是数据生产者速度太快,消费者处理不过来时,应该有一种机制让生产者“慢下来”。

Node 的 stream 模式天然利用了事件循环的暂停和恢复机制。可读流数据到达时会触发data事件,它们在 poll 阶段被调起;当你调用pause()时,相当于不再处理data事件的回调,这些回调积压在队列里;当你resume()时,再重新开始处理。理解事件循环,你就明白为什么pipe不会直接把内存撑爆——它就是在消费速度跟不上时,让源停了下来。

这个点在做大文件处理、日志导入、数据库批量写入时非常有用。我以前写过一段同步写法,从日志文件里解析逐行数据然后插入数据库,几十万行日志直接把 Node 进程卡到超时。后来改成流式读取 + 异步写入 + 暂停恢复控制,内存占用从几百兆降到了几十兆,处理速度反而快了不少。核心就是让事件循环始终保持“有节奏地干活”,而不是一次性把所有东西都压进内存和队列里。


7. 工具与调试:亲手摸一次事件循环

7.1 用代码验证执行顺序

如果你想把理论变成自己的东西,最快的方法就是自己写几个测试用例。

// 这是我最常用的一个调试组合 console.log('start'); setTimeout(() => console.log('timer1'), 0); setImmediate(() => console.log('immediate1')); Promise.resolve().then(() => { console.log('promise1'); process.nextTick(() => console.log('nextTick inside promise')); }); process.nextTick(() => console.log('nextTick1')); fs.readFile(__filename, () => { console.log('readFile'); setTimeout(() => console.log('timer inside readFile'), 0); setImmediate(() => console.log('immediate inside readFile')); process.nextTick(() => console.log('nextTick inside readFile')); });

把这段代码跑一遍,你会看到process.nextTick的各种层级优先级,也能看到 I/O 回调内部的定时器和setImmediate的顺序。我建议你一边跑一边画图,把每个输出挂到六阶段图里,你会彻底理解“阶段切换 + 插队队列”是怎么配合的。

7.2 用调试器观察任务队列

真的去“看”事件循环内部状态并不容易,但 Node 提供了不少间接手段。我常用的方法:

  • 在回调开头和结尾分别打印时间戳,测算一个阶段里所有回调的总耗时。
  • 使用process._getActiveHandles()和process._getActiveRequests()查看当前活跃的句柄和请求。
  • 用--trace-events-enabled生成 trace 文件,在 chrome://tracing 里查看事件循环各阶段的耗时分布。

这三种方法我实际用过最多的是第一种,最简单粗暴;第三种最专业,可以精确看到 timers 阶段和 poll 阶段分别花了多少毫秒。如果你排查“高并发响应慢”的问题,trace 文件能帮你一眼定位是 poll 阶段等太久,还是业务回调本身太慢。

7.3 一个派得上用场的性能小技巧

要减少事件循环的无效空转,有一个细节:别用setInterval去做高频轮询。每次轮询都会唤醒事件循环,即使无事可做也要跑一遍。优先级更高的做法是用“事件驱动”来替代轮询。比如监听文件变化用fs.watch,不要用setInterval去一遍遍读文件状态;监听消息队列用订阅,不要用轮询拉取。

再比如,Node 的poll阶段在没有定时器、没有待处理 I/O 时会阻塞等待事件。这时它不消耗 CPU。而如果你挂了一个高频setInterval,poll 阶段每次都要被 timers 阶段打断,整个循环就总处于“醒来干活”的状态,CPU 空转明显。以前我写过一个守护脚本,轮询间隔从 1 秒改成事件驱动后,CPU 使用率从 8% 直接降到接近于 0。


8. 常见问题与排查清单

8.1 输出顺序永远搞不清,怎么办

只要你还在写 JS,事件循环的乱序问题就会一直存在。我给你的排查顺序是:

  1. 先把同步代码跑出来的输出写下来。
  2. 再找process.nextTick,它的优先级最高,排最前。
  3. 再找 Promise / await 之后的微任务。
  4. 然后判断代码运行的“位置”:如果在模块顶层,setTimeout和setImmediate谁先谁后都不一定;如果在 I/O 回调里,setImmediate几乎总是先输出。
  5. 最后再补充定时器的嵌套延迟影响。

这个顺序可以作为“思维模板”,遇到任何乱序题,套进去基本都是对的。

8.2 setImmediate 和 nextTick 到底选谁

我直接给结论:

  • 需要在当前操作之后、I/O 事件之前执行回调,用process.nextTick。例如 EventEmitter 的错误处理和清理工作。
  • 需要把任务放到下一轮事件循环,不要阻塞当前阶段,用setImmediate。例如“超大列表分片处理”“让出 I/O 轮流执行”。
  • 一句话口诀:nextTick是“尽快”,setImmediate是“不要阻塞”。

8.3 会不会有“任务”被永久丢弃

正常情况下不会,事件循环会保证队列里的任务得到处理。但有一个例外:如果进程在某个回调里直接抛异常且没有捕获,Node 进程会直接退出,之后的任务全部不会执行。这就是为什么在 Node 服务里,你永远需要一个兜底的process.on('uncaughtException')和process.on('unhandledRejection'),把错误记录下来后决定是否优雅退出,而不能让整个服务突然暴毙。

另外提醒一下,不少人会把process.nextTick和 Promise 当成“可以无限追加任务”的队列用,这是一个非常危险的习惯。事件循环再能调度,也经不起无穷无尽的插队。

8.4 生产事故排查速查表

现象可能原因排查方向
定时任务执行频率暴涨回调执行时间长于间隔,任务堆积改用执行完再setTimeout,加锁
高并发下响应变慢主线程被同步 CPU 操作阻塞用 profile 定位热点,异步分片
I/O 操作迟迟不回调微任务或 nextTick 死循环检查递归代码,强制让出控制权
事件循环 CPU 空转高频轮询导致反复唤醒用事件驱动替代 setInterval
大量请求同时超时同步逻辑 + 任务堆积增加 worker,拆分阻塞逻辑

这张表是我踩过坑之后的总结。碰到诡异问题,我会先想“事件循环现在在干嘛”,而不是直接看业务代码。大部分奇怪的现象,追到根源都和循环卡顿、任务堆积、阶段顺序有关。


9. 回到开头:事件循环对你意味着什么

写了这么多,我不太想做什么总结,就分享一个经验好了。

我刚接触 Node 的时候,也不太理解为什么要研究事件循环这种东西。直到有一次在线上环境排查一个定时任务的重复执行问题,翻遍了代码也没找到毛病,最后才发现是setInterval回调里异步任务堆积导致的“连着跑”。也就是从那次开始,我痛下决心把 libuv 的阶段模型啃了一遍。啃完之后,很多以前只靠背结论的问题(比如setImmediate和setTimeout谁先执行)一下就通了,因为你不再是在猜,而是能看到代码在阶段图里的位置。

现在的我会建议所有做 JS 和 Node 的开发者,无论前端还是后端,花一个下午把事件循环这套东西彻彻底底过一遍。你不需要把每一行 C 代码都看懂,但你需要理解“调用栈空了才取任务”“微任务优先”“Node 有六个阶段”这几句核心的话。它们会把你在代码里遇见的很多“灵异事件”变成可解释、可推理的常规现象。

最后再送一个小技巧:遇到执行顺序问题,别急着问别人,先自己用console.log把关键节点打点,再对照六阶段图走两遍。走到第三遍的时候,你会发现自己已经能“看见”事件循环了。到那个状态,面试和排障都会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询