Appearance
JavaScript 内存泄漏详解
1. 什么是内存泄漏?
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏的本质
在 JavaScript 中,内存泄漏指的是:
- 不再使用的对象仍然被引用,导致垃圾回收器(GC)无法回收它们
- 程序持续占用内存,随着时间推移内存占用越来越高
- 最终可能导致页面卡顿、浏览器崩溃或应用性能严重下降
JavaScript 的内存管理机制
JavaScript 使用自动垃圾回收机制,主要采用以下算法:
javascript
// 标记-清除算法(Mark-and-Sweep)
// 1. 垃圾回收器从根对象开始遍历
// 2. 标记所有可达的对象
// 3. 清除未被标记的对象
// 根对象包括:
// - 全局变量
// - 当前执行栈中的变量
// - DOM 树中的节点内存泄漏的危害
| 危害 | 描述 |
|---|---|
| 性能下降 | 可用内存减少,GC 频繁触发,导致卡顿 |
| 页面崩溃 | 内存超出浏览器限制,页面崩溃 |
| 用户体验差 | 响应变慢,操作延迟 |
| 资源浪费 | 占用过多系统资源 |
2. 内存泄漏有哪些场景?
2.1 全局变量
javascript
// ❌ 意外的全局变量
function leak() {
bar = 'I am a global variable now'
}
// ❌ this 指向全局对象
function foo() {
this.variable = 'potential leak'
}
foo()
// ✅ 正确做法:使用严格模式
;('use strict')
function noLeak() {
let bar = 'I am local'
}2.2 闭包引起的内存泄漏
javascript
// ❌ 闭包持有大对象引用
function createClosure() {
const largeData = new Array(1000000).fill('x')
return function () {
console.log(largeData.length)
}
}
const closure = createClosure()
// largeData 无法被回收,因为闭包引用了它
// ✅ 正确做法:只保留需要的值
function createClosureFixed() {
const largeData = new Array(1000000).fill('x')
const length = largeData.length
return function () {
console.log(length)
}
}
// largeData 可以被回收2.3 未清理的定时器
javascript
// ❌ 未清理的 setInterval
class Component {
constructor() {
this.data = new Array(100000).fill('data')
this.timer = setInterval(() => {
console.log(this.data.length)
}, 1000)
}
destroy() {
// 忘记清理定时器!
}
}
// ✅ 正确做法:及时清理
class ComponentFixed {
constructor() {
this.data = new Array(100000).fill('data')
this.timer = setInterval(() => {
console.log(this.data.length)
}, 1000)
}
destroy() {
clearInterval(this.timer)
this.timer = null
}
}2.4 未清理的事件监听器
javascript
// ❌ 未移除的事件监听器
class Modal {
constructor() {
this.handleClick = this.handleClick.bind(this)
document.addEventListener('click', this.handleClick)
}
handleClick() {
console.log('clicked')
}
close() {
// 忘记移除监听器!
}
}
// ✅ 正确做法:移除事件监听器
class ModalFixed {
constructor() {
this.handleClick = this.handleClick.bind(this)
document.addEventListener('click', this.handleClick)
}
handleClick() {
console.log('clicked')
}
close() {
document.removeEventListener('click', this.handleClick)
}
}
// ✅ 使用 AbortController(现代方案)
class ModalModern {
constructor() {
this.controller = new AbortController()
document.addEventListener('click', this.handleClick.bind(this), {
signal: this.controller.signal
})
}
close() {
this.controller.abort()
}
}2.5 DOM 引用
javascript
// ❌ 保留已删除 DOM 的引用
const elements = []
const button = document.getElementById('button')
function removeButton() {
document.body.removeChild(button)
elements.push(button) // 仍然持有引用!
}
// ✅ 正确做法:清除引用
function removeButtonFixed() {
document.body.removeChild(button)
const index = elements.indexOf(button)
if (index > -1) {
elements.splice(index, 1)
}
button = null
}2.6 console.log 保留对象引用
javascript
// ❌ console.log 可能保留对象引用(开发环境)
function processData(data) {
console.log('Processing:', data) // 在某些浏览器中保留引用
return data.map(item => item.value)
}
// ✅ 生产环境移除或使用条件判断
function processDataFixed(data) {
if (process.env.NODE_ENV === 'development') {
console.log('Processing:', data)
}
return data.map(item => item.value)
}2.7 闭包中的循环引用
javascript
// ❌ 循环中的闭包问题
function attachEvents() {
const elements = document.querySelectorAll('.item')
const largeData = new Array(10000).fill('data')
elements.forEach((el, index) => {
el.addEventListener('click', function () {
console.log(largeData[index]) // 每个监听器都持有 largeData 引用
})
})
}
// ✅ 正确做法:避免不必要的引用
function attachEventsFixed() {
const elements = document.querySelectorAll('.item')
elements.forEach((el, index) => {
const neededData = getNeededValue(index)
el.addEventListener('click', function () {
console.log(neededData)
})
})
}2.8 Map/Set 中未清理的引用
javascript
// ❌ Map 中未清理的引用
const cache = new Map()
function cacheData(id, data) {
cache.set(id, data)
}
// 数据永远不会被清理
// ✅ 使用 WeakMap 或定期清理
const weakCache = new WeakMap()
function cacheDataFixed(obj, data) {
weakCache.set(obj, data) // 当 obj 被回收时,data 也会被回收
}
// ✅ 或者使用 LRU 缓存
class LRUCache {
constructor(maxSize = 100) {
this.maxSize = maxSize
this.cache = new Map()
}
set(key, value) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, value)
}
}2.9 Promise 未处理的回调
javascript
// ❌ 未处理的 Promise 可能导致内存泄漏
let pendingPromises = []
function fetchData() {
const promise = fetch('/api/data')
pendingPromises.push(promise)
return promise
}
// ✅ 正确做法:清理已完成的 Promise
function fetchDataFixed() {
const promise = fetch('/api/data').finally(() => {
const index = pendingPromises.indexOf(promise)
if (index > -1) {
pendingPromises.splice(index, 1)
}
})
return promise
}2.10 Vue/React 组件中的常见泄漏
javascript
// Vue 组件中的内存泄漏
export default {
data() {
return {
timer: null,
eventSource: null
}
},
mounted() {
this.timer = setInterval(() => {}, 1000)
this.eventSource = new EventSource('/events')
},
beforeUnmount() {
// ❌ 忘记清理
}
}
// ✅ 正确做法
export default {
data() {
return {
timer: null,
eventSource: null
}
},
mounted() {
this.timer = setInterval(() => {}, 1000)
this.eventSource = new EventSource('/events')
},
beforeUnmount() {
clearInterval(this.timer)
this.eventSource.close()
}
}javascript
// React 组件中的内存泄漏
function useLeak() {
useEffect(() => {
const timer = setInterval(() => {}, 1000)
// ❌ 没有返回清理函数
}, [])
}
// ✅ 正确做法
function useNoLeak() {
useEffect(() => {
const timer = setInterval(() => {}, 1000)
return () => clearInterval(timer)
}, [])
}3. 解决内存泄漏的方案有哪些?
3.1 使用 Chrome DevTools 检测
堆快照(Heap Snapshot)
1. 打开 DevTools -> Memory -> Heap snapshot
2. 执行操作前后各拍一次快照
3. 比较两个快照,找出增长的对象javascript
// 使用 @ 符号在堆快照中快速定位
// 例如:@12345 表示对象 ID时间线记录(Timeline)
1. 打开 DevTools -> Performance
2. 点击录制,执行操作
3. 观察内存曲线是否持续上升
4. 查看 GC 后内存是否下降分配采样(Allocation Sampling)
1. 打开 DevTools -> Memory -> Allocation sampling
2. 开始记录
3. 执行操作
4. 查看哪些函数分配了最多内存3.2 使用 Memory Profiler API
javascript
// 手动触发 GC(需要在启动 Chrome 时添加 --expose-gc 参数)
if (typeof gc === 'function') {
gc()
}
// 使用 performance.memory 查看内存使用情况
console.log({
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
})3.3 使用 WeakMap 和 WeakSet
javascript
// WeakMap 的键是弱引用,不会阻止垃圾回收
const weakMap = new WeakMap()
let obj = { id: 1 }
weakMap.set(obj, 'metadata')
obj = null // obj 可以被回收,WeakMap 中的条目也会自动清除
// 应用场景:缓存 DOM 元素的元数据
const elementMetadata = new WeakMap()
function setMetadata(element, metadata) {
elementMetadata.set(element, metadata)
}
// 当 DOM 元素被移除时,元数据自动清除3.4 使用 WeakRef 和 FinalizationRegistry
javascript
// WeakRef:弱引用对象
let target = { data: 'important' }
const weakRef = new WeakRef(target)
// 后续可以通过 deref() 获取对象
const obj = weakRef.deref()
if (obj) {
console.log(obj.data)
}
// FinalizationRegistry:对象被回收时的回调
const registry = new FinalizationRegistry(heldValue => {
console.log(`Object with id ${heldValue} was garbage collected`)
})
let obj = { id: 123 }
registry.register(obj, obj.id)
obj = null // 当 obj 被回收时,会触发回调3.5 及时清理资源
javascript
// 封装资源管理类
class ResourceManager {
constructor() {
this.timers = new Set()
this.intervals = new Set()
this.eventListeners = new Map()
this.eventSources = new Set()
}
setTimeout(callback, delay) {
const id = setTimeout(() => {
this.timers.delete(id)
callback()
}, delay)
this.timers.add(id)
return id
}
setInterval(callback, delay) {
const id = setInterval(callback, delay)
this.intervals.add(id)
return id
}
addEventListener(target, type, listener, options) {
target.addEventListener(type, listener, options)
const key = `${type}-${listener.name}`
this.eventListeners.set(key, { target, type, listener })
}
dispose() {
this.timers.forEach(id => clearTimeout(id))
this.intervals.forEach(id => clearInterval(id))
this.eventListeners.forEach(({ target, type, listener }) => {
target.removeEventListener(type, listener)
})
this.eventSources.forEach(es => es.close())
this.timers.clear()
this.intervals.clear()
this.eventListeners.clear()
this.eventSources.clear()
}
}
// 使用示例
const resources = new ResourceManager()
resources.setInterval(() => console.log('tick'), 1000)
resources.addEventListener(document, 'click', () => {})
// 清理所有资源
resources.dispose()3.6 使用对象池
javascript
// 对象池模式:重用对象,减少内存分配
class ObjectPool {
constructor(factory, initialSize = 10) {
this.factory = factory
this.pool = []
this.available = []
for (let i = 0; i < initialSize; i++) {
const obj = factory()
this.pool.push(obj)
this.available.push(obj)
}
}
acquire() {
if (this.available.length > 0) {
return this.available.pop()
}
const obj = this.factory()
this.pool.push(obj)
return obj
}
release(obj) {
if (this.pool.includes(obj)) {
this.available.push(obj)
}
}
}
// 使用示例
const vectorPool = new ObjectPool(() => ({ x: 0, y: 0 }), 100)
function processVector() {
const v = vectorPool.acquire()
v.x = Math.random()
v.y = Math.random()
// 使用 v 进行计算...
vectorPool.release(v)
}4. 怎么在代码层面规避内存泄漏
4.1 编码最佳实践
使用严格模式
javascript
'use strict'
// 防止意外创建全局变量
function foo() {
// bar = 'global' // ReferenceError: bar is not defined
let bar = 'local' // 正确
}遵循 RAII 原则(资源获取即初始化)
javascript
// 资源生命周期与对象生命周期绑定
class DatabaseConnection {
constructor(url) {
this.connection = this.connect(url)
}
connect(url) {
return new Connection(url)
}
[Symbol.dispose]() {
this.connection.close()
}
}
// 使用 using 关键字(ES2023)
{
using db = new DatabaseConnection('localhost')
// 使用 db
} // 自动调用 Symbol.dispose使用 AbortController 统一管理
javascript
class AsyncTaskManager {
constructor() {
this.controller = new AbortController()
}
fetch(url) {
return fetch(url, { signal: this.controller.signal })
}
setTimeout(callback, delay) {
const id = setTimeout(callback, delay)
this.controller.signal.addEventListener('abort', () => {
clearTimeout(id)
})
return id
}
addEventListener(target, type, listener) {
target.addEventListener(type, listener, { signal: this.controller.signal })
}
abort() {
this.controller.abort()
}
}4.2 组件生命周期管理
Vue 3 组合式 API
javascript
import { onScopeDispose, effectScope } from 'vue'
function useTimer(callback, delay) {
const scope = effectScope()
scope.run(() => {
const timer = setInterval(callback, delay)
onScopeDispose(() => {
clearInterval(timer)
})
})
return scope
}
// 使用
const timerScope = useTimer(() => console.log('tick'), 1000)
// 清理
timerScope.stop()React Hooks 清理模式
javascript
import { useEffect, useRef } from 'react'
function useWebSocket(url) {
const wsRef = useRef(null)
useEffect(() => {
const ws = new WebSocket(url)
wsRef.current = ws
return () => {
ws.close()
wsRef.current = null
}
}, [url])
return wsRef.current
}
// 自定义 Hook 清理模式
function useDisposable(factory, deps) {
const ref = useRef(null)
useEffect(() => {
ref.current = factory()
return () => {
if (ref.current && typeof ref.current.dispose === 'function') {
ref.current.dispose()
}
}
}, deps)
return ref.current
}4.3 避免闭包陷阱
javascript
// ❌ 闭包持有不需要的引用
function setupHandlers() {
const largeData = fetchData()
buttons.forEach(button => {
button.onclick = function () {
// 即使不需要 largeData,闭包也持有引用
console.log('clicked')
}
})
}
// ✅ 使用块级作用域隔离
function setupHandlersFixed() {
{
const largeData = fetchData()
// 处理 largeData
}
buttons.forEach(button => {
button.onclick = function () {
console.log('clicked')
}
})
}
// ✅ 使用箭头函数减少闭包
function setupHandlersArrow() {
const largeData = fetchData()
buttons.forEach(button => {
button.onclick = () => console.log('clicked')
})
// largeData 在此之后可以被回收
}4.4 合理使用数据结构
javascript
// 使用适当的数据结构
// 需要弱引用时使用 WeakMap
const privateData = new WeakMap()
class MyClass {
constructor() {
privateData.set(this, { secret: 'value' })
}
getSecret() {
return privateData.get(this).secret
}
}
// 需要自动清理时使用 WeakRef
class Cache {
constructor() {
this.cache = new Map()
}
set(key, value) {
this.cache.set(key, new WeakRef(value))
}
get(key) {
const ref = this.cache.get(key)
return ref?.deref()
}
}4.5 代码审查清单
markdown
## 内存泄漏代码审查清单
### 定时器
- [ ] 所有 setInterval 是否都有对应的 clearInterval?
- [ ] 所有 setTimeout 是否都需要清理?
- [ ] requestAnimationFrame 是否有 cancelAnimationFrame?
### 事件监听
- [ ] addEventListener 是否有对应的 removeEventListener?
- [ ] 自定义事件是否正确清理?
- [ ] 第三方库的事件是否正确解绑?
### 异步操作
- [ ] Promise 链是否完整?
- [ ] 取消的异步操作是否正确处理?
- [ ] WebSocket/EventSource 是否正确关闭?
### DOM 操作
- [ ] 移除 DOM 前是否解绑事件?
- [ ] DOM 引用是否在移除后置为 null?
- [ ] iframe 是否正确清理?
### 组件生命周期
- [ ] 组件销毁时是否清理所有资源?
- [ ] 路由切换时是否清理页面资源?
- [ ] 模态框关闭时是否清理内部状态?
### 闭包
- [ ] 闭包是否持有大对象引用?
- [ ] 回调函数是否可以简化?
- [ ] 是否存在循环引用?5. 代码规范 ESLint 和 TSLint 配置
5.1 ESLint 配置
安装相关插件
bash
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-no-memory-leak.eslintrc.js 配置
javascript
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['@typescript-eslint'],
rules: {
// 禁止意外的全局变量
'no-implicit-globals': 'error',
// 禁止未使用的变量
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
// 要求使用 const 声明不会被重新赋值的变量
'prefer-const': 'error',
// 禁止不必要的 .call() 和 .apply()
'no-useless-call': 'error',
// 禁止在循环中创建函数
'no-loop-func': 'warn',
// 禁止对 function 参数重新赋值
'no-param-reassign': [
'error',
{
props: false
}
],
// 禁止使用 var
'no-var': 'error',
// 强制使用块级作用域
'block-scoped-var': 'error',
// 禁止在 return 之前定义变量
'vars-on-top': 'off',
// TypeScript 特定规则
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'@typescript-eslint/prefer-readonly': 'error',
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
allowTypedFunctionExpressions: true
}
],
// 禁止 any 类型
'@typescript-eslint/no-explicit-any': 'warn',
// 要求显式返回类型
'@typescript-eslint/explicit-module-boundary-types': 'warn'
},
overrides: [
{
// Vue 组件特殊规则
files: ['*.vue'],
rules: {
// 检查生命周期钩子中的清理
}
},
{
// React 组件特殊规则
files: ['*.tsx', '*.jsx'],
rules: {
// 检查 useEffect 清理函数
}
}
]
}5.2 自定义 ESLint 规则检测内存泄漏
javascript
// custom-rules/no-memory-leak.js
module.exports = {
meta: {
type: 'problem',
docs: {
description: '检测潜在的内存泄漏',
category: 'Possible Errors',
recommended: true
},
messages: {
setIntervalWithoutClear: 'setInterval 应该有对应的 clearInterval',
addEventListenerWithoutRemove:
'addEventListener 应该有对应的 removeEventListener',
missingCleanup: '组件缺少清理逻辑'
}
},
create(context) {
const timers = new Map()
const eventListeners = new Map()
return {
CallExpression(node) {
// 检测 setInterval
if (
node.callee.name === 'setInterval' ||
(node.callee.type === 'MemberExpression' &&
node.callee.property.name === 'setInterval')
) {
const scope = context.getScope()
timers.set(node, { scope, cleared: false })
}
// 检测 clearInterval
if (
node.callee.name === 'clearInterval' ||
(node.callee.type === 'MemberExpression' &&
node.callee.property.name === 'clearInterval')
) {
// 标记已清理
}
// 检测 addEventListener
if (
node.callee.type === 'MemberExpression' &&
node.callee.property.name === 'addEventListener'
) {
const scope = context.getScope()
eventListeners.set(node, { scope, removed: false })
}
},
'Program:exit'(node) {
timers.forEach((value, key) => {
if (!value.cleared) {
context.report({
node: key,
messageId: 'setIntervalWithoutClear'
})
}
})
eventListeners.forEach((value, key) => {
if (!value.removed) {
context.report({
node: key,
messageId: 'addEventListenerWithoutRemove'
})
}
})
}
}
}
}
// 在 .eslintrc.js 中使用自定义规则
module.exports = {
plugins: ['custom-rules'],
rules: {
'custom-rules/no-memory-leak': 'error'
}
}5.3 TypeScript 配置
tsconfig.json
json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true
}
}5.4 ESLint TypeScript 规则
javascript
// .eslintrc.js 针对 TypeScript 的规则
module.exports = {
overrides: [
{
files: ['*.ts', '*.tsx'],
rules: {
// 禁止使用 any
'@typescript-eslint/no-explicit-any': 'error',
// 要求显式类型注解
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true
}
],
// 禁止非空断言
'@typescript-eslint/no-non-null-assertion': 'warn',
// 要求使用 readonly
'@typescript-eslint/prefer-readonly': 'error',
// 要求参数使用 readonly
'@typescript-eslint/prefer-readonly-parameter-types': 'warn',
// 禁止不必要的类型断言
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
// 要求成员有明确的可访问性修饰符
'@typescript-eslint/explicit-member-accessibility': [
'error',
{
accessibility: 'explicit'
}
],
// 禁止使用 require
'@typescript-eslint/no-require-imports': 'error',
// 禁止使用命名空间
'@typescript-eslint/no-namespace': 'error',
// 禁止三斜线指令
'@typescript-eslint/triple-slash-reference': 'error'
}
}
]
}5.5 React/Vue 专用规则
React ESLint 配置
javascript
// .eslintrc.js React 规则
module.exports = {
plugins: ['react-hooks'],
rules: {
// React Hooks 规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// 检测 useEffect 缺少清理函数
'react-hooks/exhaustive-deps': [
'error',
{
additionalHooks: '(useMyEffect|useAsyncEffect)'
}
]
}
}Vue ESLint 配置
javascript
// .eslintrc.js Vue 规则
module.exports = {
plugins: ['vue'],
rules: {
// Vue 生命周期检查
'vue/no-lifecycle-after-await': 'error',
'vue/no-watch-after-await': 'error',
// 组件命名规范
'vue/component-definition-name-casing': ['error', 'PascalCase'],
// 要求组件名称
'vue/multi-word-component-names': 'error',
// 禁止在模板中使用 this
'vue/this-in-template': ['error', 'never'],
// 要求 v-for 有 :key
'vue/require-v-for-key': 'error',
// 禁止在计算属性中修改状态
'vue/no-side-effects-in-computed-properties': 'error'
}
}5.6 预提交钩子配置
javascript
// lint-staged.config.js
module.exports = {
'*.{js,jsx,ts,tsx,vue}': [
'eslint --fix',
'prettier --write'
]
}
// .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
// package.json
{
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.vue",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.vue --fix",
"type-check": "tsc --noEmit"
}
}5.7 完整配置示例
javascript
// .eslintrc.js 完整配置
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:vue/vue3-recommended',
'plugin:react-hooks/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json'
},
plugins: ['@typescript-eslint', 'vue'],
rules: {
// 通用规则
'no-implicit-globals': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'prefer-const': 'error',
'no-loop-func': 'warn',
'no-var': 'error',
// TypeScript 规则
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true
}
],
'@typescript-eslint/prefer-readonly': 'error',
// Vue 规则
'vue/no-lifecycle-after-await': 'error',
'vue/no-watch-after-await': 'error',
// React Hooks 规则
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn'
},
ignorePatterns: ['node_modules', 'dist', '*.config.js']
}总结
内存泄漏是前端开发中需要重点关注的问题,通过以下方式可以有效预防和解决:
- 理解原理:了解 JavaScript 的内存管理机制
- 识别场景:熟悉常见的内存泄漏模式
- 工具检测:掌握 Chrome DevTools 的内存分析工具
- 编码规范:遵循最佳实践,及时清理资源
- 静态检查:配置 ESLint/TypeScript 规则进行预防
记住:预防胜于治疗,在编码时就注意内存管理,比事后调试更加高效。