3. JavaScript运行时与执行机制
JavaScript 是单线程语言,但浏览器和 Node.js 却能同时处理网络请求、定时器、用户交互和渲染。支撑这种“并发假象”的核心,就是运行时的事件循环(Event Loop)机制。理解它,是排查页面卡顿、回调延迟、内存泄漏等问题的前提。
调用栈、任务队列与事件循环
运行时主要由三部分组成:调用栈(Call Stack)、任务队列(Task Queue)和事件循环。调用栈负责执行同步代码,遵循后进先出;当遇到异步 API(如 setTimeout、fetch、DOM 事件)时,浏览器会把回调交给对应的 Web API 线程处理,处理完成后将回调放入任务队列。事件循环则不断检查:调用栈是否为空?若为空,就从队列中取出一个任务压入栈执行。
关键点在于:只有同步代码全部执行完,调用栈清空后,事件循环才会开始处理队列中的回调。这解释了一个常见现象——setTimeout(fn, 0) 并不会立即执行,它至少要等到当前同步脚本跑完。
宏任务与微任务:优先级差异
任务队列并非只有一条。规范将异步任务分为两类:
- 宏任务(MacroTask):
setTimeout、setInterval、setImmediate(Node)、I/O、UI 渲染、MessageChannel等。 - 微任务(MicroTask):
Promise.then/catch/finally、queueMicrotask、MutationObserver、process.nextTick(Node)。
事件循环每一轮的执行顺序是:取出一个宏任务执行 → 调用栈清空 → 清空所有微任务(包括微任务执行过程中新产生的微任务)→ 如有需要执行渲染 → 进入下一轮。也就是说,微任务的优先级高于宏任务,且会在当前宏任务结束后被“一次性清空”。
下面这段代码是面试与实战中高频出现的顺序判断题,请先自己推演,再看注释结果:
console.log('1 script start');
setTimeout(() => {
console.log('2 setTimeout'); // 宏任务
}, 0);
Promise.resolve()
.then(() => console.log('3 promise then'))
.then(() => console.log('4 promise then2'));
queueMicrotask(() => console.log('5 queueMicrotask'));
console.log('6 script end');
// 输出顺序:
// 1 script start
// 6 script end
// 3 promise then
// 5 queueMicrotask
// 4 promise then2
// 2 setTimeout
推演逻辑:同步代码先输出 1、6;调用栈清空后清空微任务队列,依次输出 3、5,而 3 的 then 又产生新微任务 4,仍在同一轮清空;微任务全部结束后,才执行宏任务 setTimeout,输出 2。
内存管理与垃圾回收
JavaScript 内存分为栈内存(存放基本类型和引用地址)和堆内存(存放对象、数组、函数等引用类型)。垃圾回收(GC)主要采用标记清除算法:从根对象(全局对象、当前调用栈中的变量)出发,标记所有可达对象,未被标记的即视为垃圾回收。现代引擎还引入分代回收,将对象分为新生代和老生代,新生代用 Scavenge 算法快速回收,老生代用标记整理减少碎片。
内存泄漏的本质是:对象已经不再需要,但仍被根对象可达引用链持有,导致 GC 无法回收。常见场景包括:
- 未清理的定时器或事件监听器,回调闭包持续引用外部大对象;
- 全局变量意外挂载,如遗漏
var/let/const造成隐式全局; - 闭包长期持有不再使用的大数组或 DOM 节点;
- DOM 已移除但 JS 仍持有其引用,形成“游离 DOM”。
性能瓶颈的定位与优化
事件循环视角下,性能瓶颈通常表现为两类:长任务阻塞和微任务饥饿。长任务指单个宏任务执行时间过长(一般超过 50ms),会阻塞渲染和后续任务,造成掉帧、点击无响应。微任务饥饿指微任务中不断产生新微任务,导致队列永远清空不完,事件循环无法进入下一轮,页面同样卡死。
定位手段上,可用 Chrome DevTools 的 Performance 面板录制,观察 Main 线程上的长条任务;用 Memory 面板拍摄堆快照(Heap Snapshot),对比多次快照中持续增长且未被回收的对象;也可用 performance.now() 在代码中打点测量关键路径耗时。
优化原则:把大计算拆成多个小任务,用 setTimeout 或 requestIdleCallback 让出主线程;对纯计算密集型任务考虑 Web Worker;及时 clearTimeout、removeEventListener、断开 IntersectionObserver;避免在微任务中无限递归。