🐞 问题背景

项目中需要实现一个中国行政地区三级联动选择器。后端返回的原始数据结构如下:

[
  {
    "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% 安全、可预测
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐