您现在的位置是:网站首页 > 组件通信中的设计模式选择文章详情
组件通信中的设计模式选择
陈川
【
JavaScript
】
58839人已围观
11413字
组件通信中的设计模式选择
组件通信是前端开发的核心问题之一,随着应用复杂度提升,如何高效、可维护地在组件间传递数据和状态成为关键挑战。不同的设计模式适用于不同场景,合理选择能显著提升代码质量。
观察者模式(Pub/Sub)
观察者模式通过解耦发布者和订阅者来实现组件通信。典型实现中,一个中央事件总线管理所有事件订阅与触发:
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
publish(event, ...args) {
const callbacks = this.events[event] || [];
callbacks.forEach(cb => cb(...args));
}
}
// 使用示例
const bus = new EventBus();
// 组件A订阅
bus.subscribe('dataUpdate', (payload) => {
console.log('Component A received:', payload);
});
// 组件B发布
bus.publish('dataUpdate', { newData: 123 });
这种模式适合全局事件通知,但过度使用可能导致事件流难以追踪。在Vue生态中,$emit/$on
机制就是观察者模式的典型实现。
中介者模式
当中介者作为通信枢纽协调多个组件交互时,能有效减少直接依赖:
class ChatRoom {
constructor() {
this.participants = {};
}
register(participant) {
this.participants[participant.name] = participant;
participant.chatRoom = this;
}
send(message, from, to) {
if (to) {
to.receive(message, from);
} else {
Object.values(this.participants).forEach(p => {
if (p !== from) p.receive(message, from);
});
}
}
}
class Participant {
constructor(name) {
this.name = name;
}
send(message, to) {
this.chatRoom.send(message, this, to);
}
receive(message, from) {
console.log(`${from.name} to ${this.name}: ${message}`);
}
}
// 使用
const room = new ChatRoom();
const john = new Participant('John');
const jane = new Participant('Jane');
room.register(john);
room.register(jane);
john.send("Hello there"); // 广播
jane.send("Hi John", john); // 私聊
这种模式在复杂表单交互或多步骤流程中特别有用,但中介者可能变成"上帝对象"。
状态管理(Redux模式)
对于全局状态共享,采用单一数据源的模式能保证状态一致性:
// 简化版Redux实现
function createStore(reducer) {
let state;
const listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
};
// 初始化状态
dispatch({ type: '@@INIT' });
return { getState, dispatch, subscribe };
}
// 使用示例
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'DECREMENT':
return { ...state, count: state.count - 1 };
default:
return state;
}
}
const store = createStore(counterReducer);
// 组件订阅
store.subscribe(() => {
console.log('State changed:', store.getState());
});
// 组件触发变更
store.dispatch({ type: 'INCREMENT' });
这种模式适合中大型应用,但会引入较多样板代码。现代实现如Redux Toolkit已大幅简化流程。
上下文API(React Context)
React的上下文系统提供组件树内的数据透传:
const UserContext = React.createContext();
function App() {
const [user, setUser] = useState({ name: 'Alice' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Header />
<Content />
</UserContext.Provider>
);
}
function Header() {
const { user } = useContext(UserContext);
return <h1>Welcome {user.name}</h1>;
}
function Content() {
const { setUser } = useContext(UserContext);
const handleClick = () => {
setUser({ name: 'Bob' });
};
return <button onClick={handleClick}>Change User</button>;
}
上下文适合主题切换、用户偏好等场景,但频繁更新的数据可能导致性能问题。
组合模式
通过children prop实现组件组合是React的推荐做法:
function Tabs({ children }) {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div className="tabs">
<div className="tab-list">
{React.Children.map(children, (child, index) => (
<button
onClick={() => setActiveIndex(index)}
className={index === activeIndex ? 'active' : ''}
>
{child.props.label}
</button>
))}
</div>
<div className="tab-content">
{React.Children.toArray(children)[activeIndex]}
</div>
</div>
);
}
function Tab({ label, children }) {
return <div>{children}</div>;
}
// 使用
function App() {
return (
<Tabs>
<Tab label="First">
<p>Content for first tab</p>
</Tab>
<Tab label="Second">
<p>Content for second tab</p>
</Tab>
</Tabs>
);
}
这种模式通过显式组合降低耦合度,适合构建可复用的UI组件库。
渲染属性(Render Props)
通过函数prop动态决定渲染内容:
class MouseTracker extends React.Component {
state = { x: 0, y: 0 };
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
};
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
}
// 使用
function App() {
return (
<MouseTracker render={({ x, y }) => (
<h1>Mouse at ({x}, {y})</h1>
)}/>
);
}
这种模式提供极大灵活性,但可能导致组件层级过深。Hooks流行后,许多场景已被自定义Hook替代。
自定义Hook共享逻辑
React Hooks可实现状态逻辑的复用:
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 组件A
function ComponentA() {
const counter = useCounter();
return (
<div>
<span>{counter.count}</span>
<button onClick={counter.increment}>+</button>
</div>
);
}
// 组件B
function ComponentB() {
const { count, decrement } = useCounter(10);
return (
<div>
<span>{count}</span>
<button onClick={decrement}>-</button>
</div>
);
}
Hooks使状态逻辑模块化且可测试,但需要遵循调用顺序规则。
命令模式封装操作
将操作封装为独立对象,支持撤销/重做等高级功能:
class CommandManager {
constructor() {
this.history = [];
this.position = -1;
}
execute(command) {
command.execute();
this.history = this.history.slice(0, this.position + 1);
this.history.push(command);
this.position++;
}
undo() {
if (this.position >= 0) {
this.history[this.position].undo();
this.position--;
}
}
redo() {
if (this.position < this.history.length - 1) {
this.position++;
this.history[this.position].execute();
}
}
}
class AddItemCommand {
constructor(list, item) {
this.list = list;
this.item = item;
this.executed = false;
}
execute() {
if (!this.executed) {
this.list.push(this.item);
this.executed = true;
}
}
undo() {
if (this.executed) {
this.list.pop();
this.executed = false;
}
}
}
// 使用
const shoppingList = [];
const manager = new CommandManager();
manager.execute(new AddItemCommand(shoppingList, 'Milk'));
manager.execute(new AddItemCommand(shoppingList, 'Eggs'));
console.log(shoppingList); // ['Milk', 'Eggs']
manager.undo();
console.log(shoppingList); // ['Milk']
manager.redo();
console.log(shoppingList); // ['Milk', 'Eggs']
这种模式在需要操作历史的场景中非常有用,如富文本编辑器。
策略模式动态选择算法
根据不同条件选择不同通信策略:
const CommunicationStrategies = {
localStorage: {
send(data) {
localStorage.setItem('crossTabData', JSON.stringify(data));
},
receive(callback) {
window.addEventListener('storage', (e) => {
if (e.key === 'crossTabData') {
callback(JSON.parse(e.newValue));
}
});
}
},
broadcastChannel: {
send(data) {
const channel = new BroadcastChannel('appChannel');
channel.postMessage(data);
setTimeout(() => channel.close(), 100);
},
receive(callback) {
const channel = new BroadcastChannel('appChannel');
channel.addEventListener('message', (e) => {
callback(e.data);
});
return () => channel.close();
}
}
};
class CrossTabCommunicator {
constructor(strategy = 'broadcastChannel') {
this.strategy = CommunicationStrategies[strategy];
this.unsubscribe = null;
}
send(data) {
this.strategy.send(data);
}
listen(callback) {
this.unsubscribe = this.strategy.receive(callback);
}
disconnect() {
this.unsubscribe?.();
}
}
// 使用
const comm = new CrossTabCommunicator('localStorage');
comm.listen(data => {
console.log('Received:', data);
});
// 另一个标签页
const comm2 = new CrossTabCommunicator('localStorage');
comm2.send({ message: 'Hello' });
策略模式让通信机制可配置,便于适应不同运行时环境。
依赖注入控制反转
通过外部注入依赖而非内部创建,提高可测试性:
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
class ApiService {
constructor(private logger: Logger) {}
fetchData() {
this.logger.log('Fetching data...');
// 实际API调用
}
}
// 使用
const logger = new ConsoleLogger();
const service = new ApiService(logger);
// 测试时可以注入Mock
class MockLogger implements Logger {
logs: string[] = [];
log(message: string) {
this.logs.push(message);
}
}
test('ApiService logs before fetching', () => {
const mockLogger = new MockLogger();
const service = new ApiService(mockLogger);
service.fetchData();
expect(mockLogger.logs).toContain('Fetching data...');
});
这种模式在需要Mock外部服务的单元测试中特别有价值。
代理模式控制访问
通过代理对象控制对目标对象的访问:
class SensitiveData {
constructor() {
this._data = [];
}
add(item) {
this._data.push(item);
}
get(index) {
return this._data[index];
}
}
class DataProxy {
constructor() {
this.data = new SensitiveData();
this.accessLog = [];
}
add(item) {
console.log('Logging add operation');
this.accessLog.push(`Added: ${item}`);
return this.data.add(item);
}
get(index) {
console.log('Logging get operation');
this.accessLog.push(`Accessed index: ${index}`);
return index < 5 ? this.data.get(index) : 'ACCESS DENIED';
}
}
// 使用
const proxy = new DataProxy();
proxy.add('Secret1');
proxy.add('Secret2');
console.log(proxy.get(0)); // 'Secret1'
console.log(proxy.get(10)); // 'ACCESS DENIED'
代理模式适合添加权限控制、日志记录等横切关注点。
备忘录模式保存状态
捕获对象状态并在需要时恢复:
class Editor {
constructor() {
this.content = '';
this.cursorPosition = 0;
}
type(text) {
this.content =
this.content.slice(0, this.cursorPosition) +
text +
this.content.slice(this.cursorPosition);
this.cursorPosition += text.length;
}
createSnapshot() {
return new EditorSnapshot(this, this.content, this.cursorPosition);
}
restore(snapshot) {
this.content = snapshot.content;
this.cursorPosition = snapshot.cursorPosition;
}
}
class EditorSnapshot {
constructor(editor, content, cursorPosition) {
this.editor = editor;
this.content = content;
this.cursorPosition = cursorPosition;
this.timestamp = Date.now();
}
}
class History {
constructor() {
this.snapshots = [];
}
push(snapshot) {
this.snapshots.push(snapshot);
}
pop() {
return this.snapshots.pop();
}
}
// 使用
const editor = new Editor();
const history = new History();
editor.type('Hello');
history.push(editor.createSnapshot());
editor.type(' World');
console.log(editor.content); // 'Hello World'
const lastState = history.pop();
editor.restore(lastState);
console.log(editor.content); // 'Hello'
这种模式在需要实现撤销栈或保存点的场景中非常实用。