在微信小程序中,当需要实现省市区、商品分类这类具有层级关系的数据选择时,原生picker的mode=multiSelector虽然能展示多列,但它要求所有列的数据都预先准备好,无法在用户切换第一列后才去加载下一列。如果数据量不大,一次性加载还能接受;但遇到某些业务场景,比如需要根据上级实时获取下级选项,或者数据存储在服务端且体积较大,就需要自定义picker来支持异步加载。本文基于<picker-view>组件,演示如何实现多级联动,并给出动态加载下级数据时显示loading状态的具体做法。

一、基于picker-view的多级联动结构
<picker-view>是微信小程序提供的基础组件,比<picker>更灵活,支持多列独立滚动。实现多级联动的核心思路是维护一个二维数组,每个子数组代表一列的数据。当某一列的值发生变化时,根据该列当前选中的项,去更新后续列的数据,并把后续列及其之后所有列的索引归零。这里使用value属性控制每列的当前选中位置,它是一个索引数组,例如[0,0]表示第一列选中第0项,第二列选中第0项。
下面给出一段基础的WXML结构,用两列来演示省市联动。每一列通过wx:for遍历columns数组中的对应子数组,数据项使用对象结构包含name和id,方便后续读取选中项的信息。
<picker-view class="picker" indicator-style="height: 50px;" value="{{value}}" bindchange="onColumnChange">
<picker-view-column>
<view wx:for="{{columns[0]}}" wx:key="*this">{{item.name}}</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{columns[1]}}" wx:key="*this">{{item.name}}</view>
</picker-view-column>
</picker-view>
对应的JS数据结构中,columns数组的第一项是第一列数据,第二项是第二列数据。初始时第一列可以放置省份数据,第二列只放一个“请选择城市”的占位项,后续根据用户操作动态替换。
data: {
value: [0, 0],
columns: [
[{ name: '北京市', id: '110000' }, { name: '河北省', id: '130000' }],
[{ name: '请选择城市' }]
]
}
这里value的初始值是[0,0],表示默认选中第一列的第一项和第二列的第一项。需要注意的是,columns中每列的数据量可以不同,这正是实现多级联动的关键——第二列的数据会随着第一列的选择而变化。
二、动态加载下级数据与loading状态实现
动态加载下级数据的关键在于监听picker-view的change事件。当用户滑动第一列导致选中项变化时,会触发onColumnChange,事件对象e.detail.value是一个索引数组,例如[1,0]表示第一列选中索引1,第二列索引0。我们需要判断哪一列发生了变化,通常比较新旧value数组。如果第一列变了,那么需要清空第二列并显示loading状态,然后发起异步请求去获取第二列数据。
为了让用户在请求期间知道系统正在加载,而不是面对一个空白列,可以在第二列放入一个占位数据,其name显示为“加载中...”,并设置一个标志位loading: true。在WXML中由于<picker-view-column>只能渲染文本,直接显示占位文本即可。同时可以配合wx.showLoading弹出全局loading,但这样做会遮挡picker,体验不如内联占位自然。所以在第二列中显示“加载中...”是更轻量的方案。
实现代码如下,同时需要处理竞态问题:用户可能连续滑动第一列,触发多个请求,后返回的旧数据可能会覆盖新数据。可以用一个递增的请求序号,每次发起请求前记录序号,响应回来后判断序号是否仍然是最新,如果不是则丢弃。
// 在data中增加requestSeq: 0
onColumnChange(e) {
const newValue = e.detail.value;
const oldValue = this.data.value;
const changedIndex = newValue.findIndex((v, i) => v !== oldValue[i]);
if (changedIndex === 0) {
// 第一列变化,重置第二列为loading
const columns = this.data.columns;
columns[1] = [{ name: '加载中...', loading: true }];
this.setData({
value: [newValue[0], 0],
columns,
requestSeq: this.data.requestSeq + 1
});
const currentSeq = this.data.requestSeq;
// 模拟异步请求
this.fetchCities(newValue[0]).then(cities => {
if (currentSeq !== this.data.requestSeq) return; // 已有更新的请求,丢弃本次结果
const columns = this.data.columns;
columns[1] = cities.length ? cities : [{ name: '暂无城市' }];
this.setData({ columns });
});
} else {
this.setData({ value: newValue });
}
},
fetchCities(provinceId) {
// 真实场景替换为wx.request
return new Promise(resolve => {
setTimeout(() => {
const map = {
'110000': [{ name: '北京市', id: '110100' }],
'130000': [{ name: '石家庄市', id: '130100' }, { name: '唐山市', id: '130200' }]
};
resolve(map[provinceId] || []);
}, 500);
});
}
代码中requestSeq每次进入第一列变化分支时自增,请求完成后比对序号,如果不等则说明用户又滑动了,丢弃旧结果。这比使用取消请求更简单可靠,因为小程序wx.request不支持真正的abort(虽然可以手动封装)。同时注意在更新value时,第二列索引重置为0,保证新数据加载后默认选中第一项。
三、完整省市联动示例与真机优化
为了更贴近真实场景,下面给出一个省市二级联动的完整可运行示例。WXML部分与前述类似,JS部分包括初始化省份数据、模拟异步请求接口、处理切换逻辑。这里展示核心代码片段,并替换为真实请求地址https://ipipp.com/cities?provinceId=。
Page({
data: {
value: [0, 0],
columns: [
[{ name: '北京市', id: '110000' }, { name: '河北省', id: '130000' }],
[{ name: '请选择城市' }]
],
requestSeq: 0,
loading: false
},
onLoad() {
// 初始加载省份数据可以直接在data中写死,也可以异步加载
},
onColumnChange(e) {
const newValue = e.detail.value;
const oldValue = this.data.value;
const changedIndex = newValue.findIndex((v, i) => v !== oldValue[i]);
if (changedIndex === 0 && newValue[0] !== undefined) {
const province = this.data.columns[0][newValue[0]];
const columns = this.data.columns.slice();
columns[1] = [{ name: '加载中...', loading: true }];
this.setData({
value: [newValue[0], 0],
columns,
requestSeq: this.data.requestSeq + 1,
loading: true
});
const currentSeq = this.data.requestSeq;
this.fetchCities(province.id).then(cities => {
if (currentSeq !== this.data.requestSeq) return;
const updatedColumns = this.data.columns.slice();
updatedColumns[1] = cities.length ? cities : [{ name: '暂无城市' }];
this.setData({
columns: updatedColumns,
loading: false
});
}).catch(() => {
if (currentSeq === this.data.requestSeq) {
const updatedColumns = this.data.columns.slice();
updatedColumns[1] = [{ name: '加载失败' }];
this.setData({ columns: updatedColumns, loading: false });
}
});
} else {
this.setData({ value: newValue });
}
},
fetchCities(provinceId) {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://ipipp.com/cities?provinceId=' + provinceId,
success: res => resolve(res.data),
fail: err => reject(err)
});
});
}
});
真机优化方面,需要注意以下几点:其一,setData不要一次性传入过大的数据,特别是columns中的数组如果包含大量对象,会导致渲染卡顿。可以考虑分页或只传递当前列所需的最小字段。其二,picker-view的列数变化会触发重新渲染,如果数据频繁变化,可以使用节流或防抖来减少setData次数。其三,对于三级或更多级联动,可以把每一级的数据放在单独的变量中,只更新发生变化的列。其四,loading状态建议使用内联占位文本而不是全局wx.showLoading,因为全局loading会阻塞与picker的交互,体验较差。最后,注意处理用户快速滑动时的请求竞态,上文已经使用requestSeq解决,也可以用时间戳或闭包变量。
通过维护二维数组、监听列变化、用占位符表示加载中、并处理请求时序,可以构建一个稳定易用的多级选择器。以上代码可根据业务需求扩展为更多层级,例如在第二列变化后再动态加载第三列,逻辑完全一致。

