← 返回思维链目录
和排障思维链的区别:排障是"线上 CPU 高怎么查"(用工具);这篇是"看到这段代码,为什么它会出问题"(读代码推理)。面试官最爱问"这段代码有什么问题",你要能像侦探一样推理。
1. 看到代码 → 为什么内存涨
现象单页应用跑久了,Chrome 内存越来越大
function setupTracker() {
const bigData = new Array(1000000).fill('x');
setInterval(() => console.log(bigData.length), 1000);
}
setupTracker(); // 调多次 → 多个定时器 + 多份 bigData
本质:bigData 被定时器回调闭包引用着,GC 沿引用链发现它"可达",所以不回收。每调用一次就多一份,越积越多。
直觉:内存涨 = 有东西该回收没回收 → 一定是有人还"引用着"它(GC 可达)→ 找是谁引用着。
🌊 顺藤摸瓜
① 闭包引用
内存涨 = 该回收没回收 → GC 为什么不回收? → 闭包:定时器回调用了 bigData,形成闭包引用 → GC 沿引用链找,bigData 可达 → 不回收
闭包持有引用可达性分析GC Root
② 定时器没清
为什么不回收? → 定时器没清除 → 定时器一直活着 → 它引用的 bigData 一直活着 → 每调一次就多一份
setInterval 未 clear多次累积闭包作用域
③ 怎么修
① clearInterval 清掉;② 不需要的数据置 null(断引用);③ 大对象别放闭包里
clearInterval置 null 断引用避免闭包持大对象
④ 类似泄漏
事件监听未 remove(回调闭包持有组件数据)/ 全局变量 / Detached DOM(DOM 移除了但 JS 还引用)/ 闭包缓存
addEventListener 未移除Detached DOMWeakMap
2. 循环里的闭包:全打印最后一个
现象循环里加事件,点击全打印同一个值
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// 输出: 3, 3, 3(不是 0,1,2)
本质:var 没有块级作用域,三个 setTimeout 的闭包共享外层同一个 i。循环结束 i=3,三个定时器看的是同一个 i,所以都打印 3。
🌊 顺藤摸瓜
① 根因
三个定时器都打印 3 → 它们引用的 i 是同一个 → var 无块级作用域,三个闭包共享一个 i → 循环结束 i=3
var 无块级作用域闭包共享变量捕获引用非值
② 怎么修
① 改 let(每次循环新建一个 i,各有各的);② IIFE 立即执行(创建独立作用域)
let 块级作用域IIFE
③ 闭包本质
闭包 = 函数 + 它引用的外层变量 → 即使外层执行完,只要闭包在,变量就不回收 → 这是闭包"保留状态"的能力,也是泄漏源头
闭包 = 函数+引用变量不回收模块化基础
3. 看到代码 → 为什么页面卡
现象列表滚动/动画卡顿
for (let i = 0; i < items.length; i++) {
items[i].style.width = '100px'; // 写:改宽度
const h = items[i].offsetHeight; // 读:读高度(强制布局)
items[i].style.height = h + 'px'; // 写:改高度
}
本质:读写交替导致强制同步布局(Layout Thrashing)。浏览器本来想"攒着一起算",你一读尺寸它被迫立刻算,写一次读一次反复重排,卡死。
🌊 顺藤摸瓜
① 根因
读写交替 → 每次读都触发强制同步布局 → 浏览器被迫立刻 Layout 给你准确值 → 写一次再读再 Layout → 反复重排
强制同步布局Layout Thrashing
② 怎么修
先读完所有,再写(批量读、批量写)→ 或用 requestAnimationFrame 把写延迟到下一帧
批量读写分离requestAnimationFrame
③ 动画卡
动画卡? → 用 transform/opacity(只触发合成,不重排)→ 别用 top/left(触发重排)→ will-change 提升合成层
重排/重绘/合成transform 不重排will-change
铁律:重排(改几何)最贵,重绘(改颜色)次之,合成(transform)最便宜。动画用 transform,读写要分离。
4. this 丢了:为什么 undefined
现象对象方法传给回调,this 变了
const obj = {
name: '小明',
say() { console.log(this.name); }
};
const fn = obj.say;
fn(); // undefined!
setTimeout(obj.say, 100); // 也 undefined
本质:this 是调用时确定的,不是定义时。obj.say() 调用时 this=obj,但 fn() / setTimeout 调用时脱离了 obj,this 就变成 window/undefined 了。
🌊 顺藤摸瓜
① 根因
this 调用时定 → obj.say() this=obj → fn() 脱离对象 → this=window/undefined → 隐式丢失
this 调用时定隐式丢失
② 怎么修
① 箭头函数(继承外层 this);② .bind(obj)(固定 this);③ const self = this
箭头函数继承bind 固定self=this
③ Vue 坑
为什么 Vue methods 别用箭头函数? → Vue 调用方法时绑定 this 为组件实例 → 箭头函数继承外层(可能是 undefined)→ 拿不到组件
Vue this 绑定React 用箭头避免丢失
5. ref 包 ECharts 卡死
现象const chart = ref(echarts.init(dom)) 切屏卡 3 秒
本质:ref 对象会深度递归响应式。ECharts 实例是巨型对象树(ZRender+Canvas+series),全部被 Proxy 劫持,初始化慢 + 每次 diff 慢。
🌊 顺藤摸瓜
① 根因
ref 深度递归 → ECharts 实例是巨型对象树 → 全部 Proxy 劫持 → 初始化慢 + 每次 diff 慢
ref 深度递归Proxy 劫持大对象性能差
② 怎么修
shallowRef(只对 .value 响应式)/ markRaw(标记永不响应式)→ 凡是 new/init 创建的有自己更新机制的对象都用 shallowRef
shallowRefmarkRaw地图/编辑器同理
③ Vue2 区别
Vue2 为什么没这问题? → Vue2 defineProperty 只遍历一层 → Vue3 Proxy 递归 → 所以 Vue3 这个坑更明显
Vue2 definePropertyVue3 Proxy 递归
总结
| 现象 | 本质 | 根因 |
| 内存涨 | 该回收没回收 | 闭包/引用未断,GC 可达 |
| 循环全打印 3 | 共享变量 | var 无块级作用域 + 闭包 |
| 页面卡 | 反复重排 | 读写交替 → 强制同步布局 |
| this undefined | 调用时确定 | 脱离对象调用,绑定丢失 |
| ref 卡 | 深度劫持 | Proxy 递归大对象 |
读代码推理 = 像侦探找线索:现象 → 为什么 → 机制 → 根因。面试官指着代码问"这段有什么问题",别只说"有问题",要讲推理 —— "我看到 X,推测是 Y 导致,因为 Z 机制"。