今天我们来聊一聊JavaScript中的核心概念:内存管理与事件机制,这对我们理解其如何工作至关重要。
系列文章目录
解密JavaScript面向对象(一):从新手到高手,手写call/bind实战
解密JavaScript面向对象(二):深入原型链,彻底搞懂面向对象精髓
解密作用域与闭包:从变量访问到闭包实战一网打尽
深度解密JavaScript异步编程:从入门到精通一站式搞定
解密浏览器事件与请求核心原理:从事件流到Fetch实战,前端通信必备指南
解密JavaScript模块化演进:从IIFE到ES Module,深入理解现代前端工程化基石
文章目录
引言:为什么需要关注内存和事件机制?
在前端开发中,我们常常专注于框架和工具的使用,但真正区分初级和高级开发者的,是对JavaScript底层机制的理解。内存泄漏对应用存在隐蔽的危害,内存管理是前端性能优化的核心要素,事件机制则与用户体验的直接关联,学习这些能协助理解现代前端框架(React/Vue)背后的底层原理,不仅能帮你写出更高效的代码,还能在面试中展现你的技术深度。
一、JavaScript垃圾回收机制深度剖析
JavaScript的内存管理遵循着经典的"分配-使用-释放"周期。当我们声明变量时,内存被分配;变量被使用时,内存被读写;当变量不再需要时,垃圾回收器会释放内存。虽然存在自动垃圾回收机制,但错误的使用方式仍然会导致内存泄漏。
1.1 引用计数算法
它的策略是跟踪记录每个变量值被使用的次数。
· 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
· 如果同一个值又被赋给另一个变量,那么引用数加 1
· 如果该变量的值被其他的值覆盖了,则引用次数减 1
· 当这个值的引用次数变为 0 的时候,说明没有变量在使用,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存。
但这个算法有个致命问题是循环引用。
// 循环引用示例
function createCycle() {
let obj1 = {};
let obj2 = {};
obj1.ref = obj2; // obj1引用obj2
obj2.ref = obj1; // obj2引用obj1,形成循环
}
1.2 标记清除(mark-sweep)算法
标记清除在 JavaScript引擎 里这种算法是最常用的,大多数浏览器的 JavaScript引擎 都在采用标记清除算法。
此算法分为标记 和 清除两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
整个标记清除算法大致过程如下:
1)垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
2)然后从各个根对象开始遍历,把不是垃圾的节点改成1
3)清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
4)最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
**优:**实现简单,就打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记。
劣:清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片,如下图。

标记整理(Mark-Compact算法就可以有效地解决。它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。

1.3 内存管理
V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。
1.3.1 新生代
当新加入对象时,它们会被存储在新生代区。新生代区域分为使用区跟空闲区。
当使用区快要被写满时,垃圾清理操作就需要执行。
在开始垃圾回收之前,新生代垃圾回收器会对使用区中的活动对象进行标记。
标记完成后,活动对象将会被复制到空闲区并进行排序。
垃圾清理阶段开始,即将非活动对象占用的空间清理掉。
最后,进行角色互换,将原来的使用区变成空闲区,将原来的空闲区变成使用区。
如果一个对象经过多次复制后依然存活,那么它将被认为是生命周期较长的对象,且会被移动到老生代中进行管理。
1.3.2 老生代
存放的是使用相对频繁且短时间无需清理回收的内容。这部分使用标记整理进行处理。
从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象。
清除阶段老生代垃圾回收器会直接将非活动对象进行清除。
1.3.3并行回收
为了减少主线程阻塞,我们在进行**垃圾回收(GC)**处理时,使用辅助进程。
1)全停顿标记(阻塞)
垃圾回收标记被放到了主进程与子进程中去处理,但最终的结果还是主进程被较长时间占用。

2)切片标记
将一次垃圾回收标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮标记。

3)三色标记
· 白色指的是未被标记的对象
· 灰色指自身被标记,成员变量(该对象的引用对象)未被标记
· 黑色指自身和成员变量皆被标记
4)惰性清理
V8 采用的是惰性清理(Lazy Sweeping)方案。标记完成后进行清理。JavaScript 脚本代码先执行清理,但无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕。
1.4 常见内存泄漏场景与解决方案
1.4.1 意外的全局变量
解决方案:使用严格模式(‘use strict’)避免意外创建全局变量
// 错误示例
function createLeak() {
leakedVar = '这是一个全局变量'; // 忘记使用var/let/const
}
// 正确做法
function safeFunction() {
const localVar = '局部变量';
}
1.4.2 闭包引起的内存泄漏
闭包是JavaScript的强大特性,但不当使用会导致内存泄漏。
最佳实践:在不需要时主动断开引用,或使用WeakMap(ES2021)等弱引用数据结构。
function createClosure() {
let largeData = new Array(1000000).fill('data');
return function() {
// 闭包保持对largeData的引用
console.log('数据长度:', largeData.length);
};
}
// 优化
function createClosure() {
let largeData = new Array(1000000).fill('data');
const weakRef = new WeakRef(largeData);
return function() {
const data = weakRef.deref();
if (data) {
console.log('数据长度:', data.length);
} else {
console.log('数据已被垃圾回收');
}
};
}
// 优化后使用示例
const closureFn = createClosure();
closureFn(); // 第一次调用可以访问数据
// 如内存存在压力,GC回收后,调用closureFn可能输出"数据已被垃圾回收"
二、JavaScript运行机制
2.1 浏览器架构:多进程架构
- 浏览器进程:负责界面显示、用户交互、子进程管理
- 渲染进程:每个标签页一个进程,负责页面渲染、脚本执行
- GPU进程:处理图形绘制任务
- 网络进程:管理网络请求
- 插件进程:运行浏览器插件
多进程主要是为了安全性和稳定性。如果一个标签页崩溃,不会影响其他标签页;插件崩溃也不会导致浏览器整体崩溃。
2.2 浏览器事件循环
JavaScript是单线程的,依靠事件循环处理异步任务。

宏任务:可以将每次执行栈执行的代码当做是一个宏任务
1)I/O
2)setTimeout
3)setInterval
4)setImmediate
5)requestAnimationFrame
微任务:当宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完
1)process.nextTick
2)MutationObserver
3)Promise.then catch finally
// 事件循环的简化模型
while (true) {
// 执行一个宏任务(栈中没有就从事件队列中获取)
// 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
const task = taskQueue.getNextTask();
if (task) {
executeTask(task);
}
// 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
const microtask = microtaskQueue.getNextTask();
while (microtask) {
executeMicrotask(microtask);
microtask = microtaskQueue.getNextTask();
}
// 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
// 下一步JS线程接管,下一个宏任务
if (needsRendering) {
updateRendering();
}
}
执行顺序练习
console.log('脚本开始'); // 同步任务
setTimeout(() => {
console.log('setTimeout'); // 宏任务
}, 0);
】
Promise.resolve().then(() => {
console.log('Promise'); // 微任务
});
console.log('脚本结束'); // 同步任务
// 输出顺序:脚本开始 → 脚本结束 → Promise → setTimeout
三、实际项目应用场景
场景1:单页应用(SPA)的内存优化
在单页应用中,路由切换时容易产生内存泄漏。组件卸载时需要清理事件监听器、定时器和异步操作。
beforeUnmount() {
// 清理工作
this.eventBus.off('someEvent', this.handler);
}
场景2:实时数据监控平台
在需要处理实时数据流的应用中,合理管理WebSocket连接和数据处理函数至关重要。
class DataMonitor {
constructor() {
this.handlers = new Set();
this.ws = null;
}
addHandler(handler) {
this.handlers.add(handler);
}
removeHandler(handler) {
this.handlers.delete(handler); // 及时移除避免内存泄漏
}
}
四、高频面试题深度解析
问题1:如何检测和解决内存泄漏?
参考答案:
内存泄漏通常发生在不再需要的对象仍然被引用,导致垃圾回收器无法回收它们。
检测常见方法:
· 使用Chrome开发者工具的Memory面板,通过Heap Snapshot对比分析内存变化;
· 重点关注Detached DOM树(已从DOM移除但被JavaScript引用的节点);
· 使用Performance面板记录内存时间线,观察内存增长趋势
其他第三方监控工具:
· Lighthouse的性能审计
· 真实用户监控(RUM)的实施
实际解决方案:
· 及时移除事件监听器、清理定时器、避免循环引用
问题2:WeakMap和WeakSet在内存管理中的特殊作用
参考答案:
WeakMap和WeakSet的键名是弱引用,不会阻止垃圾回收。
适合场景:
· 存储DOM元素的元数据(元素移除时自动回收)
· 实现私有属性(ES6类中模拟私有变量)
· 缓存系统(内存不足时自动清理)
问题3:事件循环机制是怎样的?
参考答案:
事件循环不断从任务队列中取任务执行。分为:
· 宏任务:script、setTimeout、setInterval等
· 微任务:Promise、MutationObserver、process.nextTick等
· 每执行一个宏任务,都会清空微任务队列。
五、总结
内存管理原则
定期监控:使用浏览器工具定期检查内存使用情况
及时清理:组件卸载时清理所有引用和监听器
合理设计:避免深层嵌套和循环引用
使用弱引用:在适当场景使用WeakMap/WeakSet
运行机制
理解运行机制和事件循环,,区分宏任务/微任务
下期预告
下一篇将解密函数式编程,彻底读懂纯函数,掌握函数式编程在前端框架中的体现。
如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言
1121

被折叠的 条评论
为什么被折叠?



