您现在的位置是:网站首页 > 组件通信中的设计模式选择文章详情

组件通信中的设计模式选择

组件通信中的设计模式选择

组件通信是前端开发的核心问题之一,随着应用复杂度提升,如何高效、可维护地在组件间传递数据和状态成为关键挑战。不同的设计模式适用于不同场景,合理选择能显著提升代码质量。

观察者模式(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'

这种模式在需要实现撤销栈或保存点的场景中非常实用。

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步