Cascader级联选择器的深度优化与实战技巧
项目初期的技术选型
这项目是个后台管理系统,要做一个地址选择功能,用户得从省、市、区三级里挑位置。本来想用三个下拉框联动,但产品给的原型是那种弹窗式级联菜单,点一下省,滑出市,再滑出区,体验更顺滑。没办法,只能上 Cascader。
我们项目用的是 Vue 3 + Element Plus,Cascader 组件原生就支持懒加载、异步加载,看起来很理想。一开始我还挺轻松的,觉得这功能半小时就能搞完。结果……后来三天都陷在里面,主要是数据结构和性能问题。
后端给的数据是平铺的,不是树形结构。比如:
[
{ id: 1, name: "广东省", parentId: null },
{ id: 2, name: "深圳市", parentId: 1 },
{ id: 3, name: "南山区", parentId: 2 }
]
而 Cascader 要的是 children 嵌套结构,或者用 lazy + load 函数动态加载。我一开始图省事,想着一次性拉全量数据然后前端转成树,结果数据一多,页面卡得要死,尤其是第一次渲染的时候。
最大的坑:性能问题
开始没想到数据量会这么大。全国的行政区划数据拉下来有 3000 多条,我在本地转树结构用了递归嵌套遍历,代码写得还挺优雅:
function listToTree(list) {
const map = {};
const roots = [];
list.forEach(item => {
map[item.id] = { ...item, children: [] };
});
list.forEach(item => {
if (item.parentId === null) {
roots.push(map[item.id]);
} else {
const parent = map[item.parentId];
if (parent) {
parent.children.push(map[item.id]);
}
}
});
return roots;
}
这函数本身没问题,但执行起来在主线程上跑,数据一上来,页面直接卡两秒。用户点开弹窗,半天没反应,体验极差。
后来调整了方案,改用懒加载(lazy),让 Cascader 每次只请求当前层级的数据。这样每次打开只查一级,比如先查所有省,点广东省再查下属市,点南山区再查街道(如果有)。
最终的解决方案
核心思路是:不预加载,按需请求。后端配合加了个接口,根据 parentId 返回子节点列表。前端用 Element Plus 的 lazy-load 方案。
代码如下:
<template>
<el-cascader
v-model="selectedValue"
:options="options"
:props="cascaderProps"
placeholder="请选择地区"
/>
</template>
<script setup>
import { ref } from 'vue';
const selectedValue = ref([]);
const options = ref([]);
const cascaderProps = {
lazy: true,
async lazyLoad(node, resolve) {
const { level } = node;
// 根节点:加载省级
const parentId = level === 0 ? null : node.data.id;
try {
const res = await fetch(https://jztheme.com/api/regions?parentId=${parentId});
const data = await res.json();
const nodes = data.map(item => ({
label: item.name,
value: item.id,
leaf: level >= 2 // 到区级就不往下展开了
}));
resolve(nodes);
} catch (error) {
console.error('加载失败:', error);
resolve([]);
}
}
};
</script>
这里注意我踩过好几次坑:resolve 必须调用,否则节点一直转圈;leaf 字段控制是否还能展开,我一开始忘了设,导致最后一级还能点,请求空数据。
另外,后端那个 API 接口做了缓存,相同 parentId 的请求不会每次都打数据库,减轻压力。这个优化虽然不在前端,但对整体体验帮助很大。
还有一个小毛病没彻底解决
现在方案跑起来了,但有个小问题:用户如果频繁点击不同分支,比如快速切换“广东 → 深圳”和“浙江 → 杭州”,会发出多个异步请求,可能造成节点错乱——比如杭州的区出现在深圳下面。
原因是 Cascader 内部没有自动取消前一个请求,也没有根据请求顺序做校验。我试过加个请求锁,比如用一个 pending 变量防止并发,但会导致交互变卡,用户点第二下没反应。
最后妥协了:加了个简单的防抖,延迟 200ms 发请求,同时在 resolve 前判断当前 node 是否仍是活跃节点(比如通过比对 node.path 数组)。虽然不能 100% 避免,但实测中基本看不出来。
let lastRequestId = 0;
async lazyLoad(node, resolve) {
const requestId = ++lastRequestId;
const { level } = node;
const parentId = level === 0 ? null : node.data.id;
setTimeout(async () => {
// 防止旧请求污染新节点
if (requestId !== lastRequestId) return;
try {
const res = await fetch(https://jztheme.com/api/regions?parentId=${parentId});
const data = await res.json();
const nodes = data.map(item => ({
label: item.name,
value: item.id,
leaf: level >= 2
}));
// 再次确认节点未被销毁或切换
if (requestId === lastRequestId) {
resolve(nodes);
}
} catch (error) {
if (requestId === lastRequestId) {
resolve([]);
}
}
}, 200);
}
这个方案不是最优的,但最简单,也够用。毕竟真实使用中没人会疯狂点来点去。
回顾与反思
这次用 Cascader 最大的教训就是:别高估前端处理大数据的能力。哪怕只是格式转换,几千条数据在主线程跑递归,照样卡死。能懒加载就懒加载,让用户感觉不到加载过程才是王道。
另一个经验是,组件库的功能看着强大,但实际落地时往往需要定制。Element Plus 的 Cascader 文档写得挺全,但像防抖、请求竞态这种细节根本不会提,得自己补。
做得好的地方是跟后端协作设计了合理的 API 结构,让懒加载能顺利进行。要是后端不支持按 parentId 查询,这方案就得重来。
还能优化的点:
- 前端加个简单缓存,比如把已加载过的市级节点存起来,避免重复请求
- 用 Intersection Observer 预加载下一级热点区域(比如北上广深),提升响应速度
- 移动端考虑改成 picker 滚动选择,Cascader 在小屏幕上体验一般
不过目前就这样了,上线一周没收到相关 bug 反馈,说明问题不大。
以上是我踩坑后的总结,希望对你有帮助
这个技巧的拓展用法还有很多,比如用于分类选择、组织架构、商品类目等场景,只要数据是树形结构,都能套这套懒加载逻辑。
以上是我个人对这个 Cascader 实战的完整讲解,有更优的实现方式欢迎评论区交流。毕竟谁还没几个没完全解决的小毛病呢。

暂无评论