← 返回目录

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

三级联动就是选了省自动出市,选了市自动出区,下拉框联动。

核心三步:
选省级——拿到选中的省
过滤市级——从数据里找这个省下的市
渲染市级下拉——同理渲染区级

坑:切换省要清空市和区、option value 是 ID、用 find/filter 过滤数据

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

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

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

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

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

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

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

// 取值例子:chinaData["河南省"].cities["郑州市"] → 直接拿到那 4 个区

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

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

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

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

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

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

// 1. 初始化:把所有省填进第一个下拉框 → 页面一打开,省份先列出来
// 把 chinaData 里的省名取出来当数组
const provinces = Object.keys(chinaData);

// 三个下拉框都先拿到
const $province = $("#province"),
  $city = $("#city"),
  $district = $("#district");

// 每个省造一个 option 塞进省下拉框(普通 functi
provinces.forEach(function (p) {
  $province.append('<option value="' + p + '">' + p + '</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);
  // 每个市造一个 option 填进市下拉框(普通 functi
  cities.forEach(function (city) {
    $city.append('<option value="' + city + '">' + city + '</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] || [];
  // 每个区造一个 option 填进去(普通 function
  districts.forEach(function (d) {
    $district.append('<option value="' + d + '">' + d + '</option>');
  });
});

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

5.6 两种数据加载方式

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

5.7 常见问题

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

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

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

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

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

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

本页重点

API作用参数怎么传返回/结果代码示例
on("change")上级一变就触发联动事件,函数$("#sel").on("change", function(){ ... });
$("<option>")新造一个下拉选项("<option>")option 元素let $op = $("<option>");
append把选项塞进下拉框($op)jQuery 对象$("#sel").append($op);
Object.keys取出所有省名(数据对象)省名数组let cities = Object.keys(data[prov]);
.html("") 清空先清空旧选项再生成("")下拉被清空$("#sel").html("<option>请选择</option>");