核心思路是用事件委托统一监听所有select的change,通过matches过滤目标,closest定位语义化容器,queryselector精准更新下一级选项;结构上用data-level和data-parent标识层级关系,支持动态增删与任意嵌套。

核心思路是:用事件委托统一监听所有 select 的 change,靠 matches 快速过滤目标,再用 closest 向上定位语义化容器(比如带 data-level 的父级),最后精准更新下一级选项——整套逻辑不到 50 行,不依赖框架,动态增删节点也天然兼容。
一、结构约定:用 data-level 和 data-parent 标识层级关系
HTML 结构要轻量且语义清晰,每级 select 明确标注层级和归属:
-
data-level="1"表示第一级(如省份) -
data-parent="province"表示该 select 的上级字段名(如“province”) - 下一级 select 通过
data-parent="province"与上一级 value 关联
这样避免硬编码 DOM 顺序,支持任意嵌套、跳级或动态插入新层级。
二、单次监听 + matches 过滤:只响应目标 select
不在每个 select 上重复绑定 change,而是在公共容器(如 #cascade)或 document 上监听一次:
- 用
e.target.matches('select[data-level]')判断点击/触发的是否为有效选择器 - 不匹配直接 return,避免无效执行
- 比遍历所有 select 节点更高效,尤其在大量表单项场景下
三、用 closest 定位上下文,避免 parentNode 风险
当需要根据当前 select 找到它所属的业务模块(比如一个表单区块、一个卡片),用 e.target.closest('[data-cascade-group]'):
- 自动跳过文本节点、注释节点,不需手动判断 nodeType
- 天然支持复合选择器,例如
.form-section[data-type="address"] - 比手写 while + parentNode 更安全、可读性更强
这一步不是必须,但能支撑多组级联共存(如地址+分类两套独立级联)。
四、动态填充下一级:querySelector 精准找目标 select
拿到当前 select 的 value 和 level 后,计算下一级 level,并用 querySelector 锁定唯一目标:
const nextLevel = +e.target.dataset.level + 1const nextSelect = container.querySelector(`select[data-level="${nextLevel}"][data-parent="${e.target.name}"]`)- 找到后清空并注入新 option(数据可来自对象映射或 fetch)
不用遍历所有 select,也不依赖 DOM 顺序,完全由属性驱动,扩展性强。











