ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

原生JavaScript实现省市区三级联动:从数据结构到事件绑定全解析

原生JavaScript实现省市区三级联动:从数据结构到事件绑定全解析 省市区三级联动这个需求说难不难说简单也挺能看出基本功的。我在实际项目里见过不少实现方式——有用组件库现成的有用jQuery插件改吧改吧的但真正自己用原生 JavaScript 写一遍既能理顺数据组织思路又能把事件绑定、DOM 操作这些基本功练扎实。这篇博文就把我平时写三级联动下拉菜单的完整思路、代码实现、以及踩过的坑一次讲清楚从数据结构设计到最终排错技巧保证你能直接照着写出来。1. 核心思路拆解先想清楚数据怎么组织再谈怎么渲染很多初学者上来就写代码结果写到一半发现数据结构和渲染逻辑对不上越改越乱。三级联动这种需求数据组织是骨架渲染逻辑是血肉骨架没搭好后面全是坑。1.1 为什么用嵌套对象而不是平铺数组市面上省市区数据常见的形态有两种一种是从后台接口返回的三张表另一种是前端本地维护的 JSON 数据。做前端联动我强烈建议把数据处理成嵌套结构。原因很简单联动本质就是按层级取子集。选省拿到省对象省对象里有 cities选市拿到市对象市对象里有 areas你永远只需要从当前选中节点向下取一层不需要写一堆 filter 去匹配 parentId。{ 北京市: { 北京市: [东城区, 西城区, 朝阳区] }, 广东省: { 广州市: [天河区, 越秀区, 海珠区], 深圳市: [福田区, 南山区, 罗湖区] } }这种结构写起来直观取数据也快只依赖省名就能拿到对应城市数组城市名拿到区域数组索引查找就是一次属性访问的事。1.2 两个核心操作决定整个联动逻辑联动一共就两个核心操作省变化时重建市和区市变化时重建区。这两个操作是同一个模式只是层级不同。理解了这一点你的代码就不会写得重复又冗余。我一般把“重建下级下拉框”抽成一个独立函数传入三个参数要重建的 select 元素、数据源数组、是否保留默认提示项。省变了调用一次重建出市列表顺带触发重建区列表市变了再调用一次重建出区列表。这里有个细节必须强调重建下级下拉框之前必须先清空原有的 option否则新的选项会排在旧选项后面数据一多就完全错乱了。清空的方式后面会细说。2. 数据准备真实省市区数据从哪来、怎么裁剪成前端友好的格式巧妇难为无米之炊代码写得再溜没数据也白搭。这一步把数据来源和格式化方法讲透你别自己去一个个敲那是纯浪费时间。2.1 数据来源与合规使用真实项目里省市区数据一般有两个来源一个是后台接口下发这种我就不多说了接口什么样前端就接什么样另一个是前端本地维护静态数据用于表单中不需要实时更新行政区划的场景。我用的比较多的是公开的行政区划数据包一般是按省级、市级、区级的编码和名称组织的 CSV 或 JSON 格式。下载后需要裁剪转换只保留省、市、区三级名称因为联动下拉只需要展示名称不需要邮编、区号这些冗余信息。裁剪转换这一步我建议写个一次性脚本在 Node 里跑比如把原始数据读取进来按省分组再按市分组最后组装成前面的嵌套结构。手工处理既容易漏也容易错数据量大时一定要自动化。2.2 将嵌套对象转成下拉选项的封装嵌套对象不能直接拿来塞进option中间需要一层转换。每次联动时拿到的是城市数组或区域数组数组里的元素是字符串那直接 map 成 option 就行。但有些数据源里元素是对象还带 code 字段那就得封装一层getOptionHTML之类的工具函数把对象里的 name 和 code 提取出来。两者的区别很大写代码前先弄清楚你的数据源到底长什么样不然又是一轮返工。3. 原生 JS 实现HTML 结构、事件绑定、核心函数逐个击破数据准备好了代码就水到渠成。完整代码我会拆开讲每一段都说明为什么这么写。3.1 HTML 结构设计三个 select 平铺在表单里给它们分别设置 id方便 JS 里用document.getElementById拿。这一步大家都会但有两点值得加粗提醒每个 select 初始状态只保留一个“请选择”的 optionvalue 为空字符串。这样页面加载时不至于出现一堆空选项。不要在 HTML 里写死任何省市区选项所有选项都由 JS 一次性生成。这样 html 结构干净后续数据更新也不用动 HTML。select idprovince/select select idcity/select select iddistrict/select3.2 事件绑定方式选择三级联动里有两种事件方案一种是监听每个 select 的 change 事件另一种是加一个“确认”按钮所有选项选完再触发回调。表单场景里绝大多数用前者体验更顺滑选完省立刻刷新市。事件绑定用addEventListener就行别用onchangexxx()这种内联写法原因是内联事件不好解绑多人协作时也容易把逻辑散落在 HTML 里。document.getElementById(province).addEventListener(change, function() { // 根据选中的省重建市列表和区列表 });3.3 核心函数重建下拉框重建下拉框是这套代码的心脏。函数做的事情拆开来是四步清空当前下拉框、添加默认提示项、根据数据源生成 option、把 option 塞进下拉框。清空这一步最推荐用innerHTML 它会一次性把子节点全部清掉比while(select.firstChild) { select.removeChild(select.firstChild); }这种循环写法性能更好也简洁得多。生成 option 则用字符串拼接后一次性赋值innerHTML。为什么不用createElement逐个appendChild因为频繁操作 DOM 代价高字符串拼接一次写入浏览器只做一次重绘性能明显更优数据量几十上百个 option 时差距还很微弱但这是个好习惯。function rebuildSelect(select, dataList) { select.innerHTML ; var defaultOption document.createElement(option); defaultOption.value ; defaultOption.textContent 请选择; select.appendChild(defaultOption); dataList.forEach(function(item) { var option document.createElement(option); option.value item; option.textContent item; select.appendChild(option); }); }forEach可以换成 for 循环性能差异在数据量不大时可以忽略。但如果你很有情怀或者旧环境完全不支持 ES5那 for 循环是更稳的选择。3.4 三级联动主逻辑省变化、市变化各写一个处理函数。省变化的处理函数做三件事从数据源里取出对应省的城市数组重建市下拉框然后立刻重建区下拉框。市变化处理函数做两件事从数据源里取出对应市的区域数组重建区下拉框。逻辑里最容易被忽略的是“级联重置”就是我前面说的省变的时候不光要重建市连区也得重置。很多新手漏掉这一步造成选了省之后区里边还是上一个省残留的区这就是明显的 bug。function handleProvinceChange() { var provinceName this.value; if (!provinceName) { rebuildSelect(citySelect, []); rebuildSelect(districtSelect, []); return; } var cityObj data[provinceName]; var cityList Object.keys(cityObj); rebuildSelect(citySelect, cityList); handleCityChange.call(citySelect); }这里用了Object.keys把城市的 key 取出来作为数组因为我们数据结构里城市名是对象的键。如果你用的数据源里键顺序需要保持那直接用 Object.keys 即可ES6 里对象键顺序有规则整数键按升序字符串键按插入顺序。这里城市名全是字符串所以顺序就是你数据里定义的顺序。4. 实战代码全量展示一套可以直接抄工作的完整实现上面核心逻辑拆开了现在给出完整的index.html文件内容。这个版本我综合了性能、可读性和兼容性的取舍直接复制到一个HTML文件里双击打开就能跑。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title省市区三级联动/title style select { margin-right: 10px; padding: 6px 10px; border: 1px solid #ccc; border-radius: 4px; min-width: 120px; } /style /head body h3收货地址/h3 select idprovince/select select idcity/select select iddistrict/select script // 省市区数据实际使用时可换成完整数据文件 var data { 北京市: { 北京市: [东城区, 西城区] }, 广东省: { 广州市: [天河区, 越秀区], 深圳市: [南山区, 福田区] }, 江苏省: { 南京市: [玄武区, 秦淮区], 苏州市: [姑苏区, 吴中区] } }; var provinceSelect document.getElementById(province); var citySelect document.getElementById(city); var districtSelect document.getElementById(district); function rebuildSelect(select, dataList) { select.innerHTML ; var defaultOption document.createElement(option); defaultOption.value ; defaultOption.textContent 请选择; select.appendChild(defaultOption); dataList.forEach(function(item) { var option document.createElement(option); option.value item; option.textContent item; select.appendChild(option); }); } // 初始化省列表 var provinceList Object.keys(data); rebuildSelect(provinceSelect, provinceList); provinceSelect.addEventListener(change, function() { var provinceName this.value; if (!provinceName) { rebuildSelect(citySelect, []); rebuildSelect(districtSelect, []); return; } var cityList Object.keys(data[provinceName]); rebuildSelect(citySelect, cityList); citySelect.dispatchEvent(new Event(change)); }); citySelect.addEventListener(change, function() { var provinceName provinceSelect.value; var cityName this.value; if (!cityName) { rebuildSelect(districtSelect, []); return; } var districtList data[provinceName][cityName]; rebuildSelect(districtSelect, districtList); }); /script /body /html4.1 为什么用 dispatchEvent 而不是直接调函数细心的读者会发现省变化后我用citySelect.dispatchEvent(new Event(change))来触发市变化处理而不是直接调用handleCityChange。这两种方式效果等价但用 dispatchEvent 更接近真实用户操作将来如果 citySelect 上还挂了其他监听器也能一并触发扩展性更好。新手如果直接调用函数就会出现某一天想在市变化时额外做点统计、埋点却老是少触发一次的困惑。4.2 数据量大的性能优化方案如果数据源完整包含全国几千个区县每次联动都用字符串重建所有 option 也不会卡但存在一个更优的增量方案首次加载时把省市区的所有 option 全部生成出来后续只做显示/隐藏切换。这种方案在前端表单里比较少见因为完整数据一次性渲染有点浪费不过在数据级大人的场景下切换 display 比频繁重建 DOM 快得多。代价是初始渲染耗时更长代码也更复杂。我的建议是常规表单用重建方案追求极致性能且数据量特别大时再考虑增量显隐方案。5. 常见问题与排查技巧实录这部分是实践精华很多问题不真正写过一遍根本碰不到。5.1 清空失败导致选项残留最常见的问题是重置时只清了当前下拉框没清下级下拉框比如省变了市重置了区没重置。排查方法很简单切换省之后立刻查看市和区的 option 数量哪个数量不对就用 console 打印对应 select 的 innerHTML 前 100 个字符一眼就能看出是不是旧数据残留。只要坚持一个原则省变只重置市和区市变只重置区这类 bug 就不会再犯。5.2 事件解绑失效动态重建下拉框时select 元素本身不销毁事件监听器一直挂在元素上所以通常不存在事件丢失问题。但如果你换了实现方式重建时把整个 select 外层容器都替换掉那新 select 就完全没有事件这是新手常踩的坑。解决办法是事件委托给三个 select 的父容器绑定 change 事件通过event.target.id判断目标元素再分派逻辑。事件委托的优点在于不管子元素怎么重建监听器都稳稳挂在父容器上。不过原生 select 的 change 事件不支持冒泡到document吗它支持。change 事件在 select 上触发后会冒泡委托写法完全可行。5.3 初始化时机不对数据还没渲染数据如果是异步加载的切记所有初始化动作必须放在数据就绪之后执行。否则你会看到省列表空空的辛苦写的联动逻辑就像没接上电。常见错误是把rebuildSelect(provinceSelect, Object.keys(data))写在了请求之前。排查方法也简单初始化代码处打 console 看 data 是否为空对象一般立刻能定位。5.4 兼容性注意事项现在的浏览器环境对原生 JS 支持已经非常统一但还是有两个东西值得注意一是Object.keys不兼容 IE8 及更早版本如果项目还要求支持古董环境需要加 polyfill 或者用 for in 配合 hasOwnProperty二是new Event(change)不兼容部分低版本浏览器搭配dispatchEvent使用时可能需要降级方案例如createEvent。实际业务里如果浏览器环境最低是 IE11 或现代浏览器这两点可以完全忽略但如果你的用户里有相当一部分还在用旧浏览器提前了解是为了避免线上事故。5.5 表单回显怎么处理编辑页面回显时通常后台会返回一个省/市/区的编码或名称你要按顺序回填三个下拉框。顺序千万不能乱先赋值省触发省联动生成市列表再赋值市触发市联动生成区列表最后赋值区。如果一次把所有值都赋值了却忽略联动会出现区列表还没生成、却已经把区值塞进去的诡异状态。回显函数我一般写成一个独立函数内部手动触发 change 事件复用联动的代码路径这样最稳妥。6. 扩展思路从三级联动到更复杂的联动场景三级联动只是联动场景中最基础的形态理解它的本质后扩展到四级、五级甚至树形级联都不难。联动本质上就是上级选择变化重启下级选择列表这个逻辑模式在任何层级上都成立。6.1 层级配置化方案如果希望代码能适配三级、四级不同场景可以把数据结构和重建逻辑抽象成配置驱动的形式。用一个数组保存各层级 select 的 id 和对应数据路径遍历配置生成初始列表监听 change 时按配置逐级重建后续 select。这样做的好处是换数据、增层级时只改配置不用动核心逻辑。var linkageConfig [ { selectId: province, key: province }, { selectId: city, key: city }, { selectId: district, key: district } ];这种写法适合行政区划经常扩展、产品需求还没完全定型的通用组件化场景。6.2 值存储模式名称还是编码很多项目里省市区选项除了显示名称后台只认编码。这种情况下 option 的 value 要存编码textContent 存名称联动时根据选中的编码从数据里取下一级数据。数据结构的键就不能用中文名而要用编码做键name 和 code 成对存在对象里。千万别把编码直接显示到界面上那是低级错误。数据结构设计时就把 name 和 code 分开UI 层只显示 name逻辑层只操作 code清晰又不容易错。6.3 联动组件化的封装心得把这套代码封装成独立组件、通过一个初始化函数接收容器和数据源参数时有几条心得一是所有内部状态都要保存在闭包或实例属性里别依赖全局变量二是暴露 setValue 和 getValue 方法方便表单集成三是不管内部实现多复杂对外接口越简单越好调用方只需要知道初始化传入数据、设置值、读取值这个链路。封装完成后任何表单页引入这个组件就能复用不再每个页面复制粘贴一套联动逻辑团队里其他人也用得顺手。我在实际项目里写这套联动代码写了不下五六遍每次都有新的体会。最初只会用 jQuery 插件后来数据格式不符合需求又自己写再后来遇到回显、编码存储、异步数据加载这些真实场景才慢慢把结构捋清楚。希望这篇博文能帮你少走一些弯路特别是那个级联重置的坑——记着上级变了下级连锁重置。写代码前先画一张数据结构和联动流程的小图五分钟的功夫后面能省一小时调试时间。
返回列表