跳到主要内容

组合模式

问题

什么是组合模式?如何用组合模式处理树形结构数据?前端中有哪些典型的组合模式应用场景?

答案

组合模式(Composite Pattern)是 GoF 23 种经典设计模式之一,属于结构型模式。它将对象组合成树形结构以表示"整体-部分"的层次关系,使得客户端对单个对象(叶子节点)和组合对象(容器节点)的使用具有一致性

核心价值

组合模式的核心在于:客户端无需区分操作的是叶子节点还是组合节点,统一使用相同接口。这极大简化了树形结构的操作逻辑。


核心概念与类图

组合模式包含三个核心角色:

角色说明示例
Component抽象组件,声明统一接口文件系统节点
Leaf叶子节点,没有子节点文件
Composite容器节点,包含子节点集合文件夹

树形结构示意:


TypeScript 完整实现 — 文件系统

composite/file-system.ts
// Component:抽象组件接口
interface FileSystemNode {
getName(): string;
getSize(): number;
print(indent?: string): void;
}

// Leaf:文件(叶子节点)
class File implements FileSystemNode {
constructor(
private name: string,
private size: number
) {}

getName(): string {
return this.name;
}

getSize(): number {
return this.size;
}

print(indent = ''): void {
console.log(`${indent}📄 ${this.name} (${this.size}KB)`);
}
}

// Composite:文件夹(容器节点)
class Folder implements FileSystemNode {
private children: FileSystemNode[] = [];

constructor(private name: string) {}

add(child: FileSystemNode): this {
this.children.push(child);
return this; // 支持链式调用
}

remove(child: FileSystemNode): void {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
}
}

getChild(index: number): FileSystemNode {
return this.children[index];
}

getChildren(): FileSystemNode[] {
return [...this.children];
}

getName(): string {
return this.name;
}

// 递归计算所有子节点的总大小
getSize(): number {
return this.children.reduce((total, child) => total + child.getSize(), 0);
}

print(indent = ''): void {
console.log(`${indent}📁 ${this.name} (${this.getSize()}KB)`);
this.children.forEach(child => child.print(indent + ' '));
}
}

使用示例:

composite/usage.ts
const root = new Folder('project');
const src = new Folder('src');
const components = new Folder('components');

// 链式添加文件
components
.add(new File('Button.tsx', 5))
.add(new File('Modal.tsx', 12))
.add(new File('Table.tsx', 20));

src
.add(components)
.add(new File('index.ts', 2))
.add(new File('App.tsx', 8));

root
.add(src)
.add(new File('package.json', 1))
.add(new File('tsconfig.json', 1));

// 统一接口:无论是文件还是文件夹,都调用相同方法
root.print();
// 📁 project (49KB)
// 📁 src (47KB)
// 📁 components (37KB)
// 📄 Button.tsx (5KB)
// 📄 Modal.tsx (12KB)
// 📄 Table.tsx (20KB)
// 📄 index.ts (2KB)
// 📄 App.tsx (8KB)
// 📄 package.json (1KB)
// 📄 tsconfig.json (1KB)

console.log(root.getSize()); // 49
console.log(components.getSize()); // 37

透明模式 vs 安全模式

组合模式有两种实现方式,它们的区别在于叶子节点是否暴露 add/remove 方法

透明模式

叶子节点和容器节点实现完全相同的接口,包括 addremove 等方法。叶子节点中这些方法抛出错误或空操作。

composite/transparent.ts
// 透明模式:统一接口
interface Component {
getName(): string;
getSize(): number;
add(child: Component): void;
remove(child: Component): void;
print(indent?: string): void;
}

// Leaf 也需要实现 add / remove
class TransparentFile implements Component {
constructor(private name: string, private size: number) {}

getName(): string { return this.name; }
getSize(): number { return this.size; }

add(_child: Component): void {
throw new Error('文件节点不支持 add 操作');
}

remove(_child: Component): void {
throw new Error('文件节点不支持 remove 操作');
}

print(indent = ''): void {
console.log(`${indent}📄 ${this.name}`);
}
}

安全模式

只有容器节点才有 addremove 方法,叶子节点没有这些方法。客户端需要通过类型判断来区分节点类型。

composite/safe.ts
// 安全模式:最小接口
interface SafeComponent {
getName(): string;
getSize(): number;
print(indent?: string): void;
}

// Leaf 只包含自身逻辑
class SafeFile implements SafeComponent {
constructor(private name: string, private size: number) {}
getName(): string { return this.name; }
getSize(): number { return this.size; }
print(indent = ''): void {
console.log(`${indent}📄 ${this.name}`);
}
}

// Composite 额外拥有子节点管理方法
class SafeFolder implements SafeComponent {
private children: SafeComponent[] = [];
constructor(private name: string) {}

// add/remove 仅存在于 Composite 中
add(child: SafeComponent): void {
this.children.push(child);
}

remove(child: SafeComponent): void {
const idx = this.children.indexOf(child);
if (idx !== -1) this.children.splice(idx, 1);
}

getName(): string { return this.name; }

getSize(): number {
return this.children.reduce((sum, c) => sum + c.getSize(), 0);
}

print(indent = ''): void {
console.log(`${indent}📁 ${this.name}`);
this.children.forEach(c => c.print(indent + ' '));
}
}

两种模式对比

维度透明模式安全模式
接口一致性完全统一,客户端无需区分需要区分叶子/容器类型
类型安全运行时才发现错误编译期类型检查
符合 LSP违反里氏替换(叶子抛异常)遵循
使用便利性更方便,统一操作需要类型判断
推荐场景结构简单、注重统一操作结构复杂、注重类型安全
前端推荐-TypeScript 项目优先选择
TypeScript 项目推荐安全模式

在 TypeScript 项目中,安全模式更符合类型系统的设计哲学。利用联合类型和类型守卫,可以兼顾安全性和便利性:

type TreeNode = SafeFile | SafeFolder;

function isFolder(node: TreeNode): node is SafeFolder {
return node instanceof SafeFolder;
}

function addChild(node: TreeNode, child: TreeNode): void {
if (isFolder(node)) {
node.add(child); // 类型安全,编译期检查
}
}

树形结构操作

组合模式天然与树形数据结构配合,以下是常见的树操作实现:

composite/tree-operations.ts
interface TreeNode {
name: string;
children?: TreeNode[];
}

// ========== 1. 深度优先遍历(DFS) ==========
function depthFirstTraversal(node: TreeNode, visit: (n: TreeNode) => void): void {
visit(node);
node.children?.forEach(child => depthFirstTraversal(child, visit));
}

// ========== 2. 广度优先遍历(BFS) ==========
function breadthFirstTraversal(node: TreeNode, visit: (n: TreeNode) => void): void {
const queue: TreeNode[] = [node];
while (queue.length > 0) {
const current = queue.shift()!;
visit(current);
if (current.children) {
queue.push(...current.children);
}
}
}

// ========== 3. 查找节点 ==========
function findNode(root: TreeNode, predicate: (n: TreeNode) => boolean): TreeNode | null {
if (predicate(root)) return root;
if (root.children) {
for (const child of root.children) {
const found = findNode(child, predicate);
if (found) return found;
}
}
return null;
}

// ========== 4. 过滤树 ==========
function filterTree(
node: TreeNode,
predicate: (n: TreeNode) => boolean
): TreeNode | null {
// 叶子节点:直接判断
if (!node.children || node.children.length === 0) {
return predicate(node) ? { ...node } : null;
}

// 递归过滤子节点
const filteredChildren = node.children
.map(child => filterTree(child, predicate))
.filter((child): child is TreeNode => child !== null);

// 如果自身匹配或有匹配的子节点,保留该节点
if (predicate(node) || filteredChildren.length > 0) {
return { ...node, children: filteredChildren };
}

return null;
}

// ========== 5. 计算汇总值(如文件夹总大小) ==========
interface SizedNode extends TreeNode {
size?: number;
children?: SizedNode[];
}

function calculateTotalSize(node: SizedNode): number {
if (!node.children || node.children.length === 0) {
return node.size ?? 0;
}
return node.children.reduce((sum, child) => sum + calculateTotalSize(child), 0);
}

使用示例:

composite/tree-operations-usage.ts
const tree: TreeNode = {
name: 'root',
children: [
{
name: 'src',
children: [
{ name: 'index.ts' },
{ name: 'App.tsx' },
{ name: 'components', children: [
{ name: 'Button.tsx' },
{ name: 'Modal.tsx' },
]},
],
},
{ name: 'package.json' },
],
};

// DFS:root → src → index.ts → App.tsx → components → Button.tsx → Modal.tsx → package.json
depthFirstTraversal(tree, n => console.log(n.name));

// 查找
const target = findNode(tree, n => n.name === 'Button.tsx');
console.log(target?.name); // "Button.tsx"

// 过滤:只保留 .tsx 文件
const filtered = filterTree(tree, n => n.name.endsWith('.tsx'));
// root → src → App.tsx, components → Button.tsx, Modal.tsx

前端实际应用

1. React 虚拟 DOM 树

React 的 虚拟 DOM 本身就是一棵组合模式的树。每个 ReactElement 可以是叶子节点(原生 DOM 元素、文本),也可以是容器节点(组件包含子组件):

composite/react-vdom.ts
// React Element 的简化结构 —— 典型的组合模式
interface ReactElement {
type: string | Function;
props: {
children?: ReactElement | ReactElement[];
[key: string]: unknown;
};
}

// 叶子节点
const textNode: ReactElement = {
type: 'span',
props: { children: [] },
};

// 容器节点(组合节点)
const containerNode: ReactElement = {
type: 'div',
props: {
children: [
{ type: 'h1', props: { children: [] } },
{ type: 'p', props: { children: [] } },
textNode,
],
},
};
React 中的组合模式体现
  • React.Children.map / React.Children.forEach 统一遍历子节点
  • cloneElement 可以透明地操作任意 ReactElement 节点
  • JSX 的嵌套结构天然形成树形组合

2. 文件目录树组件(递归组件)

composite/FileTree.tsx
import React, { useState } from 'react';

interface FileNode {
name: string;
type: 'file' | 'folder';
children?: FileNode[];
}

// 递归组件:既渲染叶子节点,也渲染容器节点
const FileTreeItem: React.FC<{ node: FileNode; depth?: number }> = ({
node,
depth = 0,
}) => {
const [expanded, setExpanded] = useState(false);
const paddingLeft = depth * 20;

if (node.type === 'file') {
return (
<div style={{ paddingLeft }}>
📄 {node.name}
</div>
);
}

return (
<div>
<div
style={{ paddingLeft, cursor: 'pointer' }}
onClick={() => setExpanded(!expanded)}
>
{expanded ? '📂' : '📁'} {node.name}
</div>
{/* highlight-start */}
{expanded && node.children?.map((child, i) => (
<FileTreeItem key={i} node={child} depth={depth + 1} />
))}
{/* highlight-end */}
</div>
);
};

// 使用
const fileData: FileNode = {
name: 'project',
type: 'folder',
children: [
{
name: 'src',
type: 'folder',
children: [
{ name: 'index.ts', type: 'file' },
{ name: 'App.tsx', type: 'file' },
],
},
{ name: 'package.json', type: 'file' },
],
};

const FileTree: React.FC = () => <FileTreeItem node={fileData} />;

3. 多级菜单 / 导航组件

composite/Menu.tsx
interface MenuItem {
key: string;
label: string;
icon?: string;
children?: MenuItem[];
path?: string;
}

const menuData: MenuItem[] = [
{
key: 'dashboard',
label: '仪表盘',
icon: '📊',
path: '/dashboard',
},
{
key: 'user',
label: '用户管理',
icon: '👤',
children: [
{ key: 'user-list', label: '用户列表', path: '/user/list' },
{ key: 'user-role', label: '角色管理', path: '/user/role' },
{
key: 'user-permission',
label: '权限管理',
children: [
{ key: 'perm-page', label: '页面权限', path: '/user/perm/page' },
{ key: 'perm-btn', label: '按钮权限', path: '/user/perm/btn' },
],
},
],
},
];

// 递归渲染菜单(组合模式的前端实践)
const MenuRenderer: React.FC<{ items: MenuItem[]; level?: number }> = ({
items,
level = 0,
}) => {
return (
<ul style={{ paddingLeft: level > 0 ? 16 : 0 }}>
{items.map(item => (
<li key={item.key}>
{item.children ? (
<>
<span>{item.icon} {item.label}</span>
<MenuRenderer items={item.children} level={level + 1} />
</>
) : (
<a href={item.path}>{item.icon} {item.label}</a>
)}
</li>
))}
</ul>
);
};

4. 权限树(角色 → 模块 → 操作)

composite/permission-tree.ts
interface PermissionNode {
id: string;
name: string;
type: 'module' | 'page' | 'action';
checked: boolean;
children?: PermissionNode[];
}

// 向下级联选中:选中父节点时,所有子节点也选中
function cascadeCheck(node: PermissionNode, checked: boolean): PermissionNode {
return {
...node,
checked,
children: node.children?.map(child => cascadeCheck(child, checked)),
};
}

// 向上冒泡:子节点状态改变时,更新父节点状态
function bubbleCheck(node: PermissionNode): PermissionNode {
if (!node.children || node.children.length === 0) {
return node;
}

const updatedChildren = node.children.map(bubbleCheck);
const allChecked = updatedChildren.every(c => c.checked);

return {
...node,
checked: allChecked,
children: updatedChildren,
};
}

// 收集所有选中的叶子权限
function getCheckedPermissions(node: PermissionNode): string[] {
if (!node.children || node.children.length === 0) {
return node.checked ? [node.id] : [];
}
return node.children.flatMap(child => getCheckedPermissions(child));
}

5. 表单分组(FormGroup 包含 FormItem)

composite/form-group.ts
// 抽象验证接口 —— 组合模式的 Component
interface Validatable {
validate(): ValidationResult;
getValue(): unknown;
getName(): string;
}

interface ValidationResult {
valid: boolean;
errors: string[];
}

// Leaf:单个表单字段
class FormField implements Validatable {
private value: unknown = '';
private rules: Array<(val: unknown) => string | null>;

constructor(
private name: string,
rules: Array<(val: unknown) => string | null> = []
) {
this.rules = rules;
}

setValue(val: unknown): void {
this.value = val;
}

getName(): string {
return this.name;
}

getValue(): unknown {
return this.value;
}

validate(): ValidationResult {
const errors = this.rules
.map(rule => rule(this.value))
.filter((err): err is string => err !== null);
return { valid: errors.length === 0, errors };
}
}

// Composite:表单分组
class FormGroup implements Validatable {
private fields: Validatable[] = [];

constructor(private name: string) {}

add(field: Validatable): this {
this.fields.push(field);
return this;
}

getName(): string {
return this.name;
}

getValue(): Record<string, unknown> {
const result: Record<string, unknown> = {};
this.fields.forEach(f => {
result[f.getName()] = f.getValue();
});
return result;
}

// 统一验证:递归验证所有子字段和子分组
validate(): ValidationResult {
const allErrors: string[] = [];
this.fields.forEach(field => {
const result = field.validate();
allErrors.push(...result.errors);
});
return { valid: allErrors.length === 0, errors: allErrors };
}
}

// 使用
const required = (val: unknown) => (val ? null : '此字段必填');
const minLen = (min: number) => (val: unknown) =>
typeof val === 'string' && val.length >= min ? null : `最少 ${min} 个字符`;

const basicInfo = new FormGroup('basicInfo');
const username = new FormField('username', [required, minLen(3)]);
const email = new FormField('email', [required]);
basicInfo.add(username).add(email);

const addressGroup = new FormGroup('address');
addressGroup
.add(new FormField('city', [required]))
.add(new FormField('street', [required]));

// 根表单包含多个分组
const form = new FormGroup('form');
form.add(basicInfo).add(addressGroup);

// 一次调用,递归验证整个表单树
const result = form.validate();
console.log(result); // { valid: false, errors: [...] }

组合模式与递归组件

在 React 和 Vue 中,递归组件是组合模式最直观的体现:

composite/RecursiveTree.tsx
import React, { useState, useCallback } from 'react';

interface TreeData {
id: string;
label: string;
children?: TreeData[];
}

interface TreeNodeProps {
data: TreeData;
level: number;
onSelect?: (id: string) => void;
}

// 递归组件:自身渲染自身
const TreeNode: React.FC<TreeNodeProps> = ({ data, level, onSelect }) => {
const [expanded, setExpanded] = useState(false);
const hasChildren = data.children && data.children.length > 0;

const handleToggle = useCallback(() => {
if (hasChildren) setExpanded(prev => !prev);
}, [hasChildren]);

const handleSelect = useCallback(() => {
onSelect?.(data.id);
}, [data.id, onSelect]);

return (
<div style={{ marginLeft: level * 16 }}>
<div
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
onClick={handleSelect}
>
{hasChildren && (
<span onClick={(e) => { e.stopPropagation(); handleToggle(); }}>
{expanded ? '▼' : '▶'}
</span>
)}
<span style={{ marginLeft: 4 }}>{data.label}</span>
</div>

{/* highlight-start */}
{/* 递归渲染子节点 */}
{expanded && data.children?.map(child => (
<TreeNode
key={child.id}
data={child}
level={level + 1}
onSelect={onSelect}
/>
))}
{/* highlight-end */}
</div>
);
};

// 根组件
const Tree: React.FC<{ data: TreeData[]; onSelect?: (id: string) => void }> = ({
data,
onSelect,
}) => (
<div>
{data.map(item => (
<TreeNode key={item.id} data={item} level={0} onSelect={onSelect} />
))}
</div>
);
递归组件的注意事项
  1. 必须有终止条件:当 children 为空或不存在时停止递归,否则会导致无限渲染
  2. 性能优化:深层树形数据可能导致大量组件实例。超过数百个节点时,考虑使用虚拟滚动懒加载子节点
  3. key 的唯一性:递归渲染时,确保每个节点都有全局唯一的 key,避免 Diff 算法出错

组合模式 vs 其他模式

对比维度组合模式装饰器模式迭代器模式
目的统一叶子与组合的操作接口动态添加功能顺序访问集合元素
结构树形(一对多)链式(一对一包装)线性遍历
关系整体-部分增强-被增强遍历-被遍历
常搭配迭代器(遍历树)、访问者(操作节点)组合(装饰树中的节点)组合(遍历组合结构)

常见面试问题

Q1: 组合模式的核心思想是什么?适用于什么场景?

答案

组合模式的核心思想是将对象组合成树形结构来表示"整体-部分"层次关系,使客户端对单个对象和组合对象的使用具有一致性

适用场景判断标准:

  1. 数据具有天然的树形层次结构(文件系统、组织架构、菜单、DOM 树)
  2. 需要对叶子节点和容器节点执行统一操作(遍历、计算、渲染)
  3. 客户端不希望区分处理"整体"和"部分"
// 典型适用场景:统一调用 getSize(),无论是文件还是文件夹
function printSize(node: FileSystemNode): void {
// 不需要判断是文件还是文件夹
console.log(`${node.getName()}: ${node.getSize()}KB`);
}

printSize(new File('a.ts', 10)); // a.ts: 10KB
printSize(rootFolder); // project: 49KB(自动递归计算)

Q2: 透明模式和安全模式如何选择?

答案

选择因素透明模式安全模式
语言JavaScript(弱类型)TypeScript(强类型推荐)
错误检测运行时(抛异常)编译时(类型检查)
API 简洁性客户端代码更简洁需要类型判断
违反原则违反里氏替换原则遵循 SOLID

在 TypeScript 项目中推荐安全模式,并配合类型守卫使用:

// 安全模式 + 类型守卫 = 兼顾安全性和便利性
function processNode(node: SafeFile | SafeFolder): void {
console.log(node.getName()); // 统一接口,无需判断

if (node instanceof SafeFolder) {
// 仅在确认是文件夹时才操作子节点
node.add(new SafeFile('new.ts', 1));
}
}

Q3: React 的虚拟 DOM 是如何体现组合模式的?

答案

React 虚拟 DOM 是组合模式的典型应用。每个 ReactElement 节点都有统一的结构,可以是叶子节点或包含子节点的容器:

// JSX 会被编译为 createElement 调用,形成树形结构
const element = (
<div> {/* Composite */}
<h1>Title</h1> {/* Composite(也可包含子节点) */}
<p>Text</p> {/* Composite */}
Hello World {/* Leaf:文本节点 */}
</div>
);

// 编译后的结构
const element2 = React.createElement('div', null,
React.createElement('h1', null, 'Title'), // 子组合节点
React.createElement('p', null, 'Text'), // 子组合节点
'Hello World' // 叶子节点
);

组合模式在 React 中的具体体现:

  • 统一接口React.Children.map() 能透明地遍历任意深度的子节点
  • 递归协调:Reconciler(协调器)对每个节点执行相同的 Diff 逻辑
  • Fiber 树:Fiber 架构将虚拟 DOM 树转化为 Fiber 链表树,每个 Fiber 节点也遵循统一结构

Q4: 如何实现树形数据的深度遍历和广度遍历?

答案

interface TreeNode {
value: string;
children?: TreeNode[];
}

// 深度优先(DFS)—— 递归写法
function dfs(node: TreeNode, result: string[] = []): string[] {
result.push(node.value);
node.children?.forEach(child => dfs(child, result));
return result;
}

// 深度优先(DFS)—— 迭代写法(用栈)
function dfsIterative(root: TreeNode): string[] {
const stack: TreeNode[] = [root];
const result: string[] = [];

while (stack.length > 0) {
const node = stack.pop()!;
result.push(node.value);
// 注意:从右往左压栈,保证左子节点先出栈
if (node.children) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
}
return result;
}

// 广度优先(BFS)—— 用队列
function bfs(root: TreeNode): string[] {
const queue: TreeNode[] = [root];
const result: string[] = [];

while (queue.length > 0) {
const node = queue.shift()!;
result.push(node.value);
if (node.children) {
queue.push(...node.children);
}
}
return result;
}
遍历方式数据结构顺序适用场景
DFS 递归调用栈先根后子代码简洁、树不深
DFS 迭代显式栈先根后子树很深、避免栈溢出
BFS队列逐层遍历最短路径、层级操作

Q5: 组合模式在权限系统中如何应用?

答案

权限系统天然具有树形层级:角色 → 模块 → 页面 → 操作。组合模式让权限的"授予"和"检查"操作统一处理:

interface PermNode {
id: string;
name: string;
children?: PermNode[];
}

// 获取节点及其所有后代的 ID
function getAllPermIds(node: PermNode): string[] {
const ids = [node.id];
node.children?.forEach(child => {
ids.push(...getAllPermIds(child));
});
return ids;
}

// 检查是否拥有某权限(在权限树中查找)
function hasPermission(root: PermNode, targetId: string): boolean {
if (root.id === targetId) return true;
return root.children?.some(child => hasPermission(child, targetId)) ?? false;
}

// 实际场景:用户勾选"用户管理"模块,自动获得其下所有子权限
const userModule: PermNode = {
id: 'user',
name: '用户管理',
children: [
{ id: 'user:list', name: '用户列表' },
{ id: 'user:create', name: '创建用户' },
{ id: 'user:delete', name: '删除用户' },
{
id: 'user:role',
name: '角色管理',
children: [
{ id: 'user:role:list', name: '角色列表' },
{ id: 'user:role:assign', name: '分配角色' },
],
},
],
};

console.log(getAllPermIds(userModule));
// ['user', 'user:list', 'user:create', 'user:delete', 'user:role', 'user:role:list', 'user:role:assign']

Q6: 如何避免递归组件的性能问题?

答案

递归渲染大型树结构时的性能问题及解决方案:

// 1. 懒加载子节点 —— 展开时才请求子数据
const LazyTreeNode: React.FC<{ id: string }> = ({ id }) => {
const [children, setChildren] = useState<TreeData[] | null>(null);
const [expanded, setExpanded] = useState(false);

const handleExpand = async () => {
if (!expanded && !children) {
const data = await fetchChildren(id); // 按需加载
setChildren(data);
}
setExpanded(!expanded);
};

return (
<div>
<div onClick={handleExpand}>...</div>
{expanded && children?.map(child => (
<LazyTreeNode key={child.id} id={child.id} />
))}
</div>
);
};

// 2. 虚拟滚动 —— 将树扁平化后用虚拟列表渲染
function flattenTree(nodes: TreeData[], expanded: Set<string>, level = 0): FlatNode[] {
const result: FlatNode[] = [];
for (const node of nodes) {
result.push({ ...node, level });
if (expanded.has(node.id) && node.children) {
result.push(...flattenTree(node.children, expanded, level + 1));
}
}
return result;
}

// 3. React.memo —— 避免未变化的节点重新渲染
const MemoizedTreeNode = React.memo(TreeNode, (prev, next) => {
return prev.data === next.data && prev.level === next.level;
});
优化策略适用场景效果
懒加载后端数据量大减少初始请求
虚拟滚动可见区域有限只渲染可见节点
React.memo频繁更新的树减少不必要的渲染
扁平化存储需要频繁增删改O(1) 查找和更新

Q7: 组合模式和迭代器模式如何配合?

答案

组合模式构建树形结构,迭代器模式提供统一的遍历方式。两者结合可以将遍历逻辑与数据结构解耦:

// 为组合结构实现迭代器
class IterableFolder implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(private name: string) {}

add(child: FileSystemNode): void {
this.children.push(child);
}

getName(): string { return this.name; }
getSize(): number {
return this.children.reduce((sum, c) => sum + c.getSize(), 0);
}
print(): void { /* ... */ }

// 实现 Symbol.iterator,使其可用 for...of 遍历
*[Symbol.iterator](): Generator<FileSystemNode> {
yield this; // 先返回自身
for (const child of this.children) {
if (child instanceof IterableFolder) {
yield* child; // 递归展开子文件夹
} else {
yield child;
}
}
}
}

// 使用
const root = new IterableFolder('root');
const src = new IterableFolder('src');
src.add(new File('index.ts', 2));
src.add(new File('App.tsx', 5));
root.add(src);
root.add(new File('package.json', 1));

// 用 for...of 透明遍历整棵树
for (const node of root) {
console.log(node.getName());
}
// root → src → index.ts → App.tsx → package.json

Q8: 前端树形数据通常用什么数据结构存储?扁平化 vs 嵌套各有什么优缺点?

答案

维度嵌套结构扁平化结构
数据格式{ id, children: [...] }{ id, parentId }[]
查找效率O(n) 递归O(1) Map 查找
增删改需递归定位直接 Map 操作
渲染天然适合递归组件需先构建树
适用场景展示、小型树大型树、频繁增删改

两种格式的互相转换:

interface FlatNode {
id: string;
parentId: string | null;
name: string;
}

interface TreeNodeData {
id: string;
name: string;
children: TreeNodeData[];
}

// 扁平 → 树形(O(n) 算法)
function buildTree(flatList: FlatNode[]): TreeNodeData[] {
const map = new Map<string, TreeNodeData>();
const roots: TreeNodeData[] = [];

// 第一遍:创建所有节点
flatList.forEach(item => {
map.set(item.id, { id: item.id, name: item.name, children: [] });
});

// 第二遍:建立父子关系
flatList.forEach(item => {
const node = map.get(item.id)!;
if (item.parentId === null) {
roots.push(node);
} else {
map.get(item.parentId)?.children.push(node);
}
});

return roots;
}

// 树形 → 扁平
function flattenToList(
nodes: TreeNodeData[],
parentId: string | null = null
): FlatNode[] {
return nodes.flatMap(node => [
{ id: node.id, parentId, name: node.name },
...flattenToList(node.children, node.id),
]);
}
实战建议

在状态管理中(如 Redux/Zustand),推荐使用扁平化结构 + Map 索引存储树形数据,渲染时再构建树形结构。这样更新操作为 O(1),避免深层嵌套的不可变更新难题。


Q9: 组合模式的优缺点是什么?什么时候不应该使用组合模式?

答案

优点

  1. 统一接口:客户端无需区分叶子和容器,代码简洁
  2. 扩展方便:新增节点类型无需修改已有代码(开闭原则)
  3. 自然递归:树形操作(遍历、计算、渲染)天然适合递归实现

缺点

  1. 过度泛化:统一接口可能导致叶子节点暴露不合理的方法(透明模式)
  2. 类型约束弱:难以限制组合中的子节点类型(如限制某文件夹只能放图片)
  3. 调试困难:深层嵌套的递归调用不易追踪

不适合使用的场景

  • 节点之间关系不是树形(如图结构、网状结构)
  • 叶子和容器的操作差异很大,统一接口无意义
  • 数据扁平且不需要层级关系

Q10: 如何为组合模式添加类型约束,限制子节点类型?

答案

利用 TypeScript 泛型,可以对组合模式的子节点类型施加约束:

// 泛型组合节点,约束子节点类型
class TypedComposite<T extends { getName(): string }> {
private children: T[] = [];

constructor(private name: string) {}

add(child: T): this {
this.children.push(child);
return this;
}

getChildren(): readonly T[] {
return this.children;
}

getName(): string {
return this.name;
}
}

// 实际应用:表单只能包含 FormField 或 FormGroup
class StrictFormGroup extends TypedComposite<FormField | StrictFormGroup> {
validate(): boolean {
return this.getChildren().every(child => {
if (child instanceof FormField) {
return child.validate().valid;
}
return (child as StrictFormGroup).validate();
});
}
}

// 编译期检查
const group = new StrictFormGroup('info');
group.add(new FormField('name', [required])); // OK
// group.add(new File('a.ts', 10)); // 编译错误:类型不兼容

相关链接