从一次卡顿排查说起:Vue 中 computed 与响应式数据的“死循环”陷阱
·
🐞 问题背景
项目中需要实现一个中国行政地区三级联动选择器。后端返回的原始数据结构如下:
[
{
"id": 1,
"name": "北京市",
"children": [
{
"id": 2,
"name": "朝阳区",
"children": []
}
]
}
]
而 Element Plus 的 <el-cascader> 组件要求的格式是:
{
"label": "北京市",
"value": "北京市",
"children": [...]
}
于是,我写了一个递归函数来转换数据:
// AddressSelector.vue
// 递归处理地址列表
function recurs(list) {
list.forEach(it => {
it.label = it.name;
it.value = it.name;
if (it.children) {
recurs(it.children);
}
});
}
// 获取级联选择器选项格式
const addressMenuOption = computed(() => {
recurs(addressStore.addressList)
return addressStore.addressList
})
// addressStore
async function getAddressList() {
const { data } = await listAddress();
addressList.value = data;
}
结果:页面卡顿,浏览器无响应,甚至崩溃!
经过排查:发现是 computed 死循环
引发 “infinite update loop” 错误,因为:
recurs修改了addressList.value- 修改触发响应式更新
- 更新导致
computed重新计算 - 重新计算又修改数据 → 死循环
后来取消使用计算属性computed,改用纯函数解决
async function getAddressList() {
const { data } = await listAddress();
addressList.value = data; // 存原始数据
recurs(addressList.value); // 转换数据
addressMenuOption.value = addressList.value; // 用于级联选择器
}
// 递归处理地址列表
function recurs(list) {
list.forEach(it => {
it.label = it.name;
it.value = it.name;
if (it.children) {
recurs(it.children);
}
});
}
运行项目,结果还是卡死,虽说解决上述的死循环,但是出现了新的问题。
🧠 继续排查,发现这次的问题是高频响应式触发
当我执行:
it.label = it.name;
Vue 的 Proxy 会拦截这个 set 操作,并触发依赖更新机制。即使没有组件在使用这些字段,Vue 仍然会执行完整的依赖追踪流程。
中国省市区数据通常有 3000+ 个节点,意味着:
- 3000+ 次
set拦截 - 3000+ 次依赖收集与触发
- 主线程被长时间阻塞 → 页面卡顿
这就是所谓的“高频响应式触发”问题。
接下来我又更改了代码如下:
async function getAddressList() {
const { data } = await listAddress();
addressList.value = data; // 存原始数据
recurs(data); // 转换数据
addressMenuOption.value = data; // 用于级联选择器
}
// 递归处理地址列表
function recurs(list) {
list.forEach(it => {
it.label = it.name;
it.value = it.name;
if (it.children) {
recurs(it.children);
}
});
}
我将非响应式变量data赋值给了addressMenuOption.value,终于没有卡顿了
但是就当我把代码高高兴兴的给AI做判断的时候,又分析出了潜在风险:data 已被污染
我修改了 data,而 data 已经赋值给 addressList.value,所以:
console.log(addressList.value[0].label) // '北京' ← 原始数据被污染!
如果你期望 addressList.value 是“原始数据”,这就违背了设计初衷。
最后为了代码的可预测性和可维护性,改为:
// 获取中国行政地区表选项列表
async function getAddressList() {
const { data } = await listAddress()
addressList.value = data
// 传入非响应式数据,避免高频响应式触发
addressMenuOption.value = createCascaderOptions(data)
}
// 递归处理地址列表,将其转换为级联选择器的选项格式
function createCascaderOptions(list) {
return list.map(item => ({
label: item.name,
value: item.name,
children: item.children ? createCascaderOptions(item.children) : undefined
}))
}
✅ 优点:
- 不依赖 Vue 内部实现
- 原始数据和展示数据完全分离
- 100% 安全、可预测
更多推荐


所有评论(0)