树形结构过滤是前端开发高频需求,广泛应用于菜单权限、文件管理、组织架构等场景。核心目标是:根据条件筛选节点,保留匹配节点及其完整父路径,同时保持树形结构不变、不修改原始数据。
一、核心问题定义
输入输出
- 输入:树形结构数组(每个节点含
name和可选children字段)、过滤条件(如名称匹配) - 输出:新树形结构,仅包含「匹配节点 + 必要祖先节点」
数据格式示例
javascript
const tree = [
{ name: 'A' },
{ name: 'B', children: [{ name: 'A' }, { name: 'D', children: [] }] },
{ name: 'C' }
];
核心约束
- 保持层级关系:匹配节点的所有祖先必须保留
- 数据不可变:不修改原始树,返回全新结构
- 完整性:匹配节点的子节点无需过滤(仅需保留路径)
二、最优实现方案
递归核心实现(推荐)
javascript
/**
* 树形结构过滤:保留匹配节点及其父路径
* @param {Array} tree - 原始树形结构
* @param {string} filterName - 过滤名称
* @returns {Array} 过滤后的新树
*/
function filterTree(tree, filterName) {
const filterNode = (node) => {
// 1. 节点自身匹配:直接返回(含所有子节点)
if (node.name === filterName) return { ...node };
// 2. 递归处理子节点
if (node.children?.length) {
const filteredChildren = node.children.map(filterNode).filter(Boolean);
// 子节点有匹配项:保留当前节点 + 过滤后的子节点
if (filteredChildren.length) {
return { ...node, children: filteredChildren };
}
}
// 3. 无匹配:返回null(后续过滤)
return null;
};
return tree.map(filterNode).filter(Boolean);
}
算法特性
- 时间复杂度:O(n)(遍历所有节点一次)
- 空间复杂度:O(h)(h为树高,递归栈开销)
迭代实现(大数据量适配)
解决深层树递归栈溢出问题:
javascript
function filterTreeIterative(tree, filterName) {
const stack = tree.map(node => ({ node, path: [] }));
const result = [];
while (stack.length) {
const { node, path } = stack.pop();
// 匹配节点:重建完整路径
if (node.name === filterName) {
let currentLevel = result;
path.forEach(ancestor => {
let exist = currentLevel.find(item => item.name === ancestor.name);
if (!exist) currentLevel.push(exist = { ...ancestor, children: [] });
currentLevel = exist.children;
});
currentLevel.push({ ...node });
}
// 子节点入栈(保持深度优先)
node.children?.forEach(child => {
stack.push({ node: child, path: [...path, node] });
});
}
return result;
}
三、灵活扩展:通用过滤函数
支持模糊匹配、自定义子节点字段,适配复杂场景:
javascript
/**
* 通用树形过滤函数
* @param {Array} tree - 原始树
* @param {Function} predicate - 匹配函数(返回boolean)
* @param {Object} options - 配置:{ childrenKey: 子节点字段名 }
* @returns {Array} 过滤后新树
*/
function filterTreeUniversal(tree, predicate, options = {}) {
const { childrenKey = 'children' } = options;
const filterNode = (node) => {
if (predicate(node)) return { ...node };
const children = node[childrenKey];
if (children?.length) {
const filteredChildren = children.map(filterNode).filter(Boolean);
if (filteredChildren.length) {
return { ...node, [childrenKey]: filteredChildren };
}
}
return null;
};
return tree.map(filterNode).filter(Boolean);
}
// 用法示例:模糊匹配含"A"的节点
const result = filterTreeUniversal(tree, node =>
node.name.toLowerCase().includes('a')
);
四、前端实战场景
1. 文件管理器搜索
javascript
const fileTree = [
{ name: 'src', children: [
{ name: 'components', children: [{ name: 'Button.js' }, { name: 'Modal.js' }] },
{ name: 'utils.js' }
]}
];
// 搜索"Button":返回 src/components/Button.js 完整路径
const searchResult = filterTree(fileTree, 'Button.js');
2. 组织架构筛选
javascript
const orgTree = [
{ name: '技术部', children: [
{ name: '前端组', children: [{ name: '张三' }, { name: '李四' }] },
{ name: '后端组', children: [{ name: '王五' }] }
]}
];
// 筛选"前端"相关:保留技术部→前端组→所有成员
const deptResult = filterTreeUniversal(orgTree, node =>
node.name.includes('前端')
);
3. 权限菜单过滤
javascript
const allMenus = [
{ name: '系统管理', perm: 'admin', children: [
{ name: '用户管理', perm: 'user:manage' },
{ name: '角色管理', perm: 'role:manage' }
]}
];
// 根据用户权限过滤
const userPerms = ['user:manage'];
const menuResult = filterTreeUniversal(
allMenus,
node => userPerms.includes(node.perm),
{ childrenKey: 'children' }
);
五、性能优化要点
- 大数据量:使用迭代实现代替递归,避免栈溢出(树高>1000时)
- 频繁过滤:缓存计算结果,或使用虚拟列表只渲染可见节点
- 复杂匹配:提前预处理节点(如小写化名称),减少匹配时计算
- 按需加载:结合后端接口,只请求匹配路径的节点数据
六、测试关键用例
javascript
// 1. 匹配叶子节点:返回完整路径
expect(filterTree(tree, 'D')[0].name).toBe('B');
expect(filterTree(tree, 'D')[0].children[0].name).toBe('D');
// 2. 匹配父节点:返回该节点及所有子节点
expect(filterTree(tree, 'B')[0].children.length).toBe(2);
// 3. 无匹配项:返回空数组
expect(filterTree(tree, 'X').length).toBe(0);
// 4. 不修改原始数据
const original = JSON.parse(JSON.stringify(tree));
filterTree(tree, 'A');
expect(tree).toEqual(original);
总结
树形结构过滤的核心是「递归遍历 + 路径保留」,关键要把握三点:
- 优先使用递归实现(简洁高效),大数据量场景切换迭代方案
- 严格遵循数据不可变原则,避免副作用
- 通过通用函数封装,适配不同业务场景(匹配规则、节点结构)