必须先扁平化再渲染树形组件,因直接递归渲染易触发深度响应式开销导致卡顿,且搜索、权限判断等逻辑在扁平数组中快一个数量级;需手写容错flattenTree函数,支持自定义键名、层级、父ID、循环引用防护,并在store getter中缓存复用。

为什么必须先扁平化再渲染树形组件
直接递归渲染嵌套结构在 uni-app 里容易触发深度响应式代理开销,尤其在 H5 或小程序中滚动时卡顿明显;更关键的是,搜索、权限判断、多选状态同步等逻辑,遍历扁平数组比反复递归查 children 快一个数量级。你拿到的后端数据大概率是 children 嵌套格式,但组件内部真正需要的是一维列表 + 每项带 level、parentId、isLeaf 这类元信息。
用递归函数实现可靠扁平化(推荐)
别依赖第三方库,手写一个带容错的 flattenTree 就够用。重点不是“能跑”,而是应对字段名不统一、children 为 null/undefined、循环引用等真实场景。
- 默认用
id和children作为键名,但必须支持自定义:idKey、childrenKey、parentKey - 每层递归传入当前
level(从 0 开始),并记录parentId,方便后续做父子联动 - 显式判断
Array.isArray(node[childrenKey]),避免null或undefined导致报错Cannot read property 'length' of undefined - 加个
visited = new Set()防循环引用(虽然少见,但后端误配时真会炸)
function flattenTree(treeData, options = {}) {
const { idKey = 'id', childrenKey = 'children', parentKey = 'pid', level = 0, parentId = null } = options
const result = []
const visited = new Set()
<p>function walk(nodes, currentLevel, currentParentId) {
if (!Array.isArray(nodes)) return
nodes.forEach(node => {
const id = node[idKey]
if (!id || visited.has(id)) return
visited.add(id)</p><pre class='brush:php;toolbar:false;'> result.push({
...node,
level: currentLevel,
[parentKey]: currentParentId,
isLeaf: !Array.isArray(node[childrenKey]) || node[childrenKey].length === 0
})
if (Array.isArray(node[childrenKey]) && node[childrenKey].length > 0) {
walk(node[childrenKey], currentLevel + 1, id)
}
})}
walk(Array.isArray(treeData) ? treeData : [treeData], level, parentId) return result }
uni-app 中调用时机与缓存建议
扁平化不是一次性的转换动作,而是数据流中的关键中间态。你得想清楚它在哪发生、是否可复用。
- 不要在
onLoad里扁平化后直接赋值给data—— 这会导致响应式丢失,改用ref或reactive包裹返回结果 - 如果数据来自 vuex/pinia,建议在 store 的 getter 里做扁平化,而不是每次组件
computed里重复执行 - 对同一份原始树数据,多次调用
flattenTree是浪费。可以用JSON.stringify(treeData)做简易缓存 key(注意只用于开发期或小数据量) - 如果树节点超过 500 个,考虑加个 loading 状态,避免白屏 —— 扁平化本身是同步操作,但用户感知上需要反馈
扁平化后怎么渲染出“树形感”
扁平数组本身没有视觉层级,靠的是样式缩进 + 展开图标控制。这里最容易忽略的是:缩进值和展开状态必须严格对应,否则点击错位、子项错乱。
-
:style="{ paddingLeft: item.level * 24 + 'px' }"是最稳妥的缩进方式,别用 margin 或 flex gap,小程序里兼容性差 - 展开图标(+ / −)的状态必须绑定到独立的
expandedItems数组,而不是靠item.children.length判断 —— 因为扁平化后item已经没有children字段了 - 判断“是否有子项”请用
item.isLeaf === false,而不是item.children—— 后者在扁平数组里根本不存在 - 点击展开时,只把当前节点 ID 加入
expandedItems;收起时,要同时移除它以及所有以它为祖先的子项 ID(需提前构建好父子映射表)


















