← 返回目录

5. 三级联动原理与动态渲染

把这页想成"填表选地址":选了省,市里自动跳出这个省的城市;选了市,区里自动跳出这个市的区——三个下拉框一层牵着一层,这就是三级联动。它的核心就三句话:数据驱动(数据长这样,下拉就长这样)、事件监听(上级一变就触发)、动态渲染(下级重新生成 option)。最关键的一个动作:上级一变,下级先清空再生成

5.1 先跑起来:省 → 市 → 区 联动

试着切换省份,观察城市、区县被"清空 → 重新生成"的过程。

5.2 核心机制(原理流程图)

用户选择省份"河南省" 触发 change 事件 读取选中的值 根据省份去查城市列表 清空城市下拉框 遍历城市生成 option 同时清空区县下拉框(旧区县已经无效了)

注意最后一步,这是新手最容易漏的:上级一变,下级全部清空。如果你只重新生成了城市、忘了清空区县,页面上就会残留上一个省份的旧区县,甚至反复切换后出现一大堆重复选项。

5.3 层级数据结构:对象嵌套(小数据推荐)

const chinaData = {                  // 一份写死在前端的省市区数据:一层套一层
  "河南省": {                         // 先按省份名一层
    cities: {                         // 省里套着它的城市
      "郑州市": ["金水区", "中原区", "二七区", "惠济区"],  // 每个城市里套着它的区县数组
      "洛阳市": ["涧西区", "西工区", "老城区", "洛龙区"]
    }
  },
  "山东省": {
    cities: {
      "济南市": ["历下区", "市中区", "槐荫区", "历城区"],
      "青岛市": ["市南区", "市北区", "黄岛区", "崂山区"]
    }
  },
  "广东省": {
    cities: {
      "广州市": ["越秀区", "海珠区", "荔湾区", "天河区"],
      "深圳市": ["福田区", "罗湖区", "南山区", "宝安区"]
    }
  }
};
// 取值例子:chinaData["河南省"].cities["郑州市"] → 直接拿到那 4 个区

这是"一层套一层"的对象:省 → 市里套着城市,城市 → 里套着区县数组。取值非常直观:chinaData["河南省"].cities["郑州市"] 直接就是金水区那一串。缺点是没法直接传给后端(后端不认这种嵌套),只适合小数据、写死在前端的场景。

5.4 层级数据结构:扁平数组 + parentId(对接后端用)

const regions = [                    // 一份扁平数据:所有省市县平铺在一个数组里
  { id: 1, name: "河南省", parentId: 0, level: 1 }, // 省 → parentId 是 0 表示最顶层
  { id: 2, name: "山东省", parentId: 0, level: 1 },
  { id: 11, name: "郑州市", parentId: 1, level: 2 }, // 市:parentId 指向省的 id → 郑州市归 id=1 的省管
  { id: 12, name: "洛阳市", parentId: 1, level: 2 },
  { id: 111, name: "金水区", parentId: 11, level: 3 }, // 区:parentId 指向市的 id → 金水区归 id=11 的市管
  { id: 112, name: "中原区", parentId: 11, level: 3 }
];

// 查找子级:凡是 parentId 等于选中项 id 的,就是它的子级 → 谁的"爸"是它,谁就是它孩子
const cities = regions.filter(r => r.parentId === selectedProvinceId); // 把省 id 当爹,挑出所有归它管的市
对比 对象嵌套 扁平数组 + parentId
可读性 一眼看清层级 要靠 parentId 自己理关系
查找效率 直接对象取值,快 要遍历 filter 找
数据量 只适合小数据 能撑大数据
对接后端 没法直接传 标准接口格式

parentId 的思路是:每个元素记一下"我爸是谁的 id",查子级就 filter 谁的 parentId 等于选中项的 id。真实项目里后端返回的基本都是这种扁平格式。

5.5 完整代码(change 事件驱动,两段模板一模一样)

// 1. 初始化:把所有省填进第一个下拉框 → 页面一打开,省份先列出来
const provinces = Object.keys(chinaData);  // 把 chinaData 里的省名取出来当数组
const $province = $("#province"),          // 三个下拉框都先拿到
  $city = $("#city"),
  $district = $("#district");
provinces.forEach(p => $province.append(`<option value="${p}">${p}</option>`)); // 每个省造一个 option 塞进省下拉框

// 2. 省份 change → 更新城市(同时清空区县)→ 选了省,市和区要跟着变
$province.on("change", function () {
  const selectedProvince = $(this).val();  // 取出现在选的是哪个省
  $city.html('<option value="">请选择城市</option>'); // ★ 先清空城市 → 把旧城市选项全丢掉
  $district.html('<option value="">请选择区县</option>'); // ★ 旧区县失效,一并清空 → 市都换了,区更不能留
  if (!selectedProvince) return; // 判空,没选就停 → 选回"请选择"就到此为止
  const cities = Object.keys(chinaData[selectedProvince].cities); // 从数据里查出这个省有哪些市
  cities.forEach(city => $city.append(`<option value="${city}">${city}</option>`)); // 每个市造一个 option 填进市下拉框
});

// 3. 城市 change → 更新区县(和上面是同一个模板)→ 选了市,区跟着变
$city.on("change", function () {
  const selectedProvince = $province.val();  // 取现在选的省
  const selectedCity = $(this).val();        // 取现在选的市
  $district.html('<option value="">请选择区县</option>'); // 先清空区县
  if (!selectedProvince || !selectedCity) return;  // 省或市没选就停
  const districts = chinaData[selectedProvince].cities[selectedCity] || []; // ★ || [] 兜底 → 没录这个市的区时,给个空数组别报错
  districts.forEach(d => $district.append(`<option value="${d}">${d}</option>`)); // 每个区造一个 option 填进去
});

你发现没有,省和市两段代码模板一模一样清空下级 → 判空 return → 查数据 → 遍历生成 option。把这个模板背下来,四级、五级联动就是再加一层的事。注意那句 || []:如果某个城市没有区县数据,chinaData[省].cities[市] 会是 undefined,直接遍历就报错,用 || [] 兜个空数组就安全了。

5.6 两种数据加载方式

方式 做法 适用
一次性加载 页面刚打开就把省市区全部数据放进前端 数据量小(<1000 条)、层级固定
按需加载 选省后才 AJAX 请求城市,选市后才请求区县 数据量大、层级深、要动态更新
// 按需加载示例:选了省份,才去请求它的城市 → 不一次性全拉,选啥要啥
$province.on("change", function () {
  const provinceId = $(this).val();   // 取选的省 id
  if (!provinceId) return;            // 没选就别请求
  $.ajax({                            // 找后端要这个省的城市
    url: "/api/cities",               // 城市接口
    data: { provinceId },             // 把省 id 发给后端
    success: res => {                // 后端把城市列表送回来
      if (res.code === 0) {           // 0 表示成功
        $city.html('<option value="">请选择城市</option>'); // 先清空城市旧选项
        res.data.forEach(c => $city.append(`<option value="${c.id}">${c.name}</option>`)); // 把后端给的城市填进去
        $district.html('<option value="">请选择区县</option>'); // 顺手把区县也清空 → 上级变了下级也得清
      }
    }
  });
});

5.7 常见问题

现象 原因 解决
选省后城市没变化 数据键名和选项值对不上(大小写、空格) 检查键名和 value 必须完全一致
区县下拉是空的 该城市根本没有区县数据 || [] 兜底,空时提示"暂无区县"
多切换几次出现重复选项 每次都 append,却没先清空 更新前先 .html("") 清空

5.8 实战:用在哪 / 常见坑 / 怎么解决

① 可能在什么地方用:收货地址的省市区选择、企业注册的地区选择、商品分类的多级下拉、酒店预订的城市/商圈选择——凡是"一层选完下一层才知道有什么"的表单都是联动。

② 常见的问题:切换省份后区县残留旧数据(忘了清空区县);多切换几次选项越堆越多(每次 append 前没 innerHTML 清空);选了城市区县空了还报错(数据为空时忘了 || [] 兜底);选省后城市一动不动(数据键名和 option 的 value 对不上)。

③ 解决思路:选省后在控制台打印一下 $(this).val 和查出来的城市数组,看是不是空的、键名对不对;选项重复就检查有没有先清空;区县报错就在取值那行加 || []。记住模板:清空下级 → 判空 → 查数据 → 生成 option

一句话:数据驱动 + change 事件 + 动态渲染 option;上级一变,下级先清空再生成;取不到数据用 || [] 兜底。