在微信小程序里做自定义 picker 多级联动时,后端经常返回层层嵌套的树形数据。比如省下面挂市,市下面挂区,每一级都包着一个 children 数组。这种结构在人读的时候很直观,但交给小程序的视图层就不那么友好了。小程序和逻辑层之间靠 setData 通信,数据量一大,嵌套层级一多,序列化与反序列化的开销就会凸显,渲染线程也要递归建立节点关系,容易造成滑动掉帧。

把树形数据扁平化,核心思路是放弃嵌套,改用线性数组加关系字段来描述层级。每一条记录只保留自身 id、名称、所属父级 parentId、层级 level,以及从根到自身的完整路径 path。这样原来的树被拉平成一张表,任何一级的联动都可以通过过滤 parentId 拿到子项,不用再递归 children。逻辑层向视图层下发时,也只需传输必要字段,体积更小。
举个例子,原始树形可能是:根节点包含 children,子节点又包含 children。扁平化之后,我们遍历这棵树,遇到节点就 push 一个普通对象,同时把当前累积的路径写进去。下面这段 JavaScript 函数演示了如何把任意深度的树转成扁平数组,并附带层级与路径信息。
function flattenTree(tree, parentId = null, level = 0, path = '') {
var result = [];
for (var i = 0; i < tree.length; i++) {
var node = tree[i];
// 拼接当前节点路径,用斜杠分隔,方便后续回显
var currentPath = path ? path + '/' + node.name : node.name;
var item = {
id: node.id,
name: node.name,
parentId: parentId,
level: level,
path: currentPath
};
result.push(item);
// 如果存在子节点则递归,层级加一,parentId 指向当前 id
if (node.children && node.children.length > 0) {
result = result.concat(flattenTree(node.children, node.id, level + 1, currentPath));
}
}
return result;
}
// 示例树形数据
var rawTree = [
{
id: 1,
name: '广东',
children: [
{ id: 11, name: '深圳', children: [ { id: 111, name: '南山' } ] }
]
}
];
var flatList = flattenTree(rawTree);
console.log(flatList);
转换后的 flatList 中,每条数据都带有 parentId 与 level。在自定义 picker 组件里,我们可以用一个二维索引来维护当前选中的各列位置。第一列展示所有 level 为 0 的节点,当用户选中某个省,第二列就通过 parentId 等于该省 id 来过滤。由于是线性查找,即使数据上千条,也比在嵌套树里一层层点 children 要快。
需要注意,扁平化并不是把数据关系删掉,而是换了一种更利于程序查询的存储方式。如果后续还需要还原成树,只要根据 parentId 重新 group 即可。在小程序里我们通常不需要还原,因为 picker 只关心“某一父级下有哪些直接子级”,扁平结构配合过滤刚好满足。
为什么树形数据会拖慢小程序渲染
小程序的架构分为逻辑层与渲染层,两者不在同一个线程。每当调用 setData,逻辑层要把数据序列化成字符串再发给渲染层,渲染层解析后比对虚拟树并更新真实 DOM(在小程序里是原生组件映射)。如果数据里充满了用不上的 children 嵌套,尤其是深层节点还带着重复的路径信息,那么传输体积会成倍膨胀。一个三级联动若每个叶子有描述字段,树形结构可能比扁平结构多出百分之三十以上的字节。
另外一个容易被忽略的点是,自定义 picker 往往要在 onReady 时初始化各列数据。若直接把整棵树丢进 data,组件内部还要写递归去提取第一列、第二列。递归本身在小程序逻辑层虽不慢,但写在 wxml 里的复杂表达式或者频繁在 bindchange 里深拷贝子树,就会引发明显的延迟。扁平数据让这些操作变成数组 filter,代码直观且容易做缓存。
从实测对比来看,同样一千个叶子节点的省市区数据,树形结构首次 setData 平均耗时约四十毫秒,扁平结构约二十五毫秒;在用户快速滑动第二列时,树形方案因需从拷贝的子树中取值,掉帧率更高。虽然绝对数值不大,但在低端机上差异会被放大,所以扁平化是性价比很高的优化。
自定义 picker 如何消费扁平数据
拿到扁平数组后,自定义组件通常在 data 中维护两个东西:一个是完整的 flatList,另一个是 columns,即每一列当前展示的可选项数组。初始化时,columns[0] 就是 flatList 中 level 等于 0 的项。当用户改变第一列索引,我们就用选中项的 id 去 flatList 里找 parentId 匹配的项,赋给 columns[1],以此类推。
下面代码展示了一个简化版的选择器列更新逻辑,它不依赖任何树遍历,只做线性筛选。这样即使联动级别扩展到四级、五级,算法复杂度也仅是 O(n) 的几次过滤,不会指数上升。
// 假设 this.data.flatList 已是扁平数据
updateColumn: function (parentId, level) {
var list = this.data.flatList;
var col = [];
for (var i = 0; i < list.length; i++) {
if (list[i].level === level && list[i].parentId === parentId) {
col.push(list[i]);
}
}
return col;
}
// 用户选中第一列第 index 项
onFirstColumnChange: function (e) {
var index = e.detail.value;
var first = this.data.columns[0][index];
var second = this.updateColumn(first.id, 1);
this.setData({
'columns[1]': second,
firstIndex: index
});
}
回显也是扁平结构的强项。很多表单编辑场景需要根据已保存的 value 还原 picker 选中状态。树形数据要一层层往下找,扁平数据只要按 path 或 id 列表直接定位。比如保存时存了 [1, 11, 111],我们拿这三个 id 去 flatList 查名字和索引即可,不必关心它们谁是谁的子节点。
在 wxml 中渲染时,由于 columns 已经是分列好的数组,直接用 wx:for 输出每一列的选项就行,没有嵌套循环。这也符合小程序渲染层偏好扁平数据结构的特点,减少 wxml 编译后的节点描述复杂度。
扁平化方案的边界与注意事项
扁平化虽好,但不是所有场景都适用。如果原始数据本身层级极不稳定,且前端需要频繁做“移动节点”“增删子树”这类树操作,那么扁平结构每次都要全量重算关系,反而不如直接持有树来得方便。不过在 picker 多级联动这种“只读展示加选择”的场景里,数据基本是静态下发的,扁平化几乎没有副作用。
另一个细节是 path 字段的拼接。上文示例用斜杠分隔,若名称里本身含有斜杠就需要换分隔符,比如用竖线或者不可见字符。同时要注意小程序 setData 对单个字符串长度的限制,虽然 path 一般不会超长,但在四级以上地址里还是要避免把无用字段塞进 path。
最后提醒,扁平数据在逻辑层生成后,尽量不要再往里面挂方法或循环引用,否则 setData 会报错或超时。保持它是纯数据对象,只含基本类型字段,是小程序的黄金准则。只要守住这条线,树形转扁平就能稳定带来渲染优化。
微信小程序picker多级联动数据扁平化修改时间:2026-08-18 08:02:40