← 返回思维链目录

前端代码现象 → 根因

看到一段代码或现象,像侦探一样推理出为什么

和排障思维链的区别:排障是"线上 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 机制"。