级联选择器是多级关联数据展示的经典组件,典型场景包括省市区选择、商品类目选择、部门架构选择等。在实际项目中,这类数据往往层级深、体量大,一次性拉取全部数据既浪费带宽也会拖慢首屏渲染,因此动态加载(懒加载)几乎是标配。与此同时,当层级达到四级以上时,用户逐级点开的成本很高,搜索定位功能能明显提升体验。本文将以 Element Plus 的 el-cascader 组件为例,讲解在 Vue 3 中如何实现这两个能力。

一、级联选择器的基本结构与数据格式
Element Plus 的级联选择器要求数据以树形结构组织,每个节点包含 value、label 和可选的 children 字段。如果是静态数据,直接把整棵树传给 options 属性即可,组件会自动根据 children 渲染下级面板。但在懒加载模式下,我们不传 children,而是通过 props.lazy 和 props.lazyLoad 告诉组件如何异步获取子节点。
下面是一个典型的数据节点结构:value 是节点的唯一标识,通常对应后端的主键或编码;label 是展示文案;leaf 用来标记是否为叶子节点,组件依据它决定是否继续触发加载。需要特别注意的是,在懒加载模式下 leaf 字段不是可选项,如果没有正确设置,组件会一直显示加载状态或者出现空面板。
// 级联选择器数据节点结构示例
{
value: 110000, // 节点唯一标识
label: '北京市', // 展示文案
leaf: false // 是否叶子节点,懒加载时必填
}组件的基本用法如下,通过 v-model 绑定选中值数组,options 绑定数据源:
<template>
<el-cascader
v-model="selectedValue"
:props="cascaderProps"
placeholder="请选择所在地区"
clearable
/>
</template>
<script setup>
import { ref } from 'vue'
const selectedValue = ref([])
const cascaderProps = {
lazy: true,
lazyLoad: loadNode
}
async function loadNode(node, resolve) {
// node.level 为 0 时表示根级别
const level = node.level
const parentId = level === 0 ? 0 : node.value
const list = await fetchRegion(parentId)
resolve(list.map(item => ({
value: item.id,
label: item.name,
leaf: level >= 3 // 假设最多四级
})))
}
</script>二、动态加载的实现细节与缓存优化
lazyLoad 函数接收当前节点对象 node 和一个 resolve 回调。首次打开选择器时,node.level 为 0,此时应加载第一级数据;用户点击某个节点展开下一级时,组件会再次调用 lazyLoad,并把被点击节点的 value 传给我们。resolve 接收子节点数组后渲染面板。这个机制的关键在于:每次展开都会触发一次请求,如果不做缓存,同一节点被反复展开时会重复请求接口。
一个简单有效的做法是在组件外维护一个 Map 缓存,以父节点 id 为 key,请求结果为 value。命中缓存直接 resolve,未命中再发请求。这样即使用户来回切换面板,也不会产生多余的网络开销。示例代码如下:
const cache = new Map()
async function loadNode(node, resolve) {
const parentId = node.level === 0 ? 0 : node.value
if (cache.has(parentId)) {
resolve(cache.get(parentId))
return
}
const list = await fetchRegion(parentId)
const nodes = list.map(item => ({
value: item.id,
label: item.name,
leaf: item.isLeaf
}))
cache.set(parentId, nodes)
resolve(nodes)
}还有几个容易踩的坑值得注意。第一,leaf 判断逻辑要与真实数据一致,如果后端没有返回层级信息,可以在前端根据 value 的编码规则推断,例如行政区划编码为六位时前两位代表省级。第二,如果接口较慢,建议在 lazyLoad 里做异常捕获,请求失败时调用 resolve([]) 结束加载状态,避免面板一直转圈。第三,回显已选值时组件会自动沿着路径逐级调用 lazyLoad,这正是缓存的另一个价值所在,可以显著加快回显速度。
三、搜索功能的实现思路
懒加载模式下的搜索是一个难点,因为前端只持有已加载过的部分节点,无法在本地完成全量匹配。业界常见做法有两种:一是远程搜索,二是本地搜索。远程搜索指用户输入关键字后,调用后端接口返回匹配的节点完整路径,前端把路径写入组件;本地搜索则只适用于数据已全量加载的场景,通过设置 filter-method 自定义过滤函数实现。
远程搜索的实现思路是监听输入,防抖后请求搜索接口,拿到匹配结果的路径数组后,通过 options 手动注入或直接调用组件暴露的方法设置选中值。由于懒加载组件内部不维护完整树,最稳妥的方案是搜索命中后把该节点的完整路径(value 数组)直接赋给 v-model,组件回显时会自动触发各级 lazyLoad 拉取路径上的节点。
import { ref } from 'vue'
import { ElCascader } from 'element-plus'
const keyword = ref('')
const cascaderRef = ref()
const searchResults = ref([])
// 防抖处理,避免频繁请求
let timer = null
function handleSearch(kw) {
clearTimeout(timer)
timer = setTimeout(async () => {
if (!kw.trim()) {
searchResults.value = []
return
}
// 后端返回匹配节点的完整路径,例如 [110000, 110100, 110101]
searchResults.value = await searchRegion(kw)
}, 300)
}如果数据量不大且已全量获取,本地搜索则简单得多。只要给 el-cascader 加上 filterable 属性,组件默认会按 label 做拼音和文本匹配;如需自定义匹配规则,可通过 props 传入 filter-method。对于全量数据场景,还可以配合 searchable 高亮匹配文案,体验更好。实际选型时建议:层级不超过三级且总量在一万条以内可考虑全量加载加本地搜索,超出这个规模就应使用懒加载加远程搜索的组合。
四、完整可用的组合示例
最后把动态加载、缓存和远程搜索整合成一个完整示例。模板部分包含级联选择器和一个远程搜索结果下拉,逻辑部分封装了请求、缓存与防抖,可以直接迁移到真实项目中使用。
<template>
<div class="region-picker">
<el-cascader
ref="cascaderRef"
v-model="selected"
:props="cascaderProps"
placeholder="选择或搜索地区"
clearable
/>
<div>
</template>
<script setup>
import { ref, onUnmounted } from 'vue'
const selected = ref([])
const cache = new Map()
let timer = null
const cascaderProps = {
lazy: true,
lazyLoad: async (node, resolve) => {
const parentId = node.level === 0 ? 0 : node.value
try {
if (cache.has(parentId)) {
return resolve(cache.get(parentId))
}
const list = await fetchRegion(parentId)
const nodes = list.map(i => ({
value: i.id,
label: i.name,
leaf: i.level >= 4
}))
cache.set(parentId, nodes)
resolve(nodes)
} catch (e) {
resolve([]) // 出错时结束加载动画
}
}
}
onUnmounted(() => clearTimeout(timer))
</script>使用时还需注意组件销毁后及时清理缓存和定时器,避免内存泄漏。如果项目中有多个地方复用该选择器,建议把上述逻辑抽成 useRegionCascader 组合式函数,通过参数区分不同的数据源,这样既保持了逻辑内聚,也便于单元测试。综合来看,动态加载解决的是数据体积问题,搜索解决的是查找效率问题,两者结合才能让级联选择器在大数据量场景下真正好用。