Skip to content

在 React 中,组件之间的通信是一个常见且重要的话题。根据组件之间的关系不同,常见的通信场景包括:

  1. 父组件向子组件通信:使用 props 直接传递
  2. 子组件向父组件通信:通过在父组件中定义回调函数,并传递给子组件使用的方式完成。
  3. 跨级组件通信:可以使用 Context、Redux 等集中式状态管理工具,避免多层级“props drilling”。
  4. 非嵌套关系的组件通信:若能找到公共父组件,可通过提升状态至公共父组件共享;若无法,则可以考虑使用全局状态管理或事件总线。

1. 父组件向子组件通信

1.1 使用 Props

  • 使用场景
    当父组件需要将某些数据或函数传递给子组件时,最常用的方式是通过父组件给子组件传递 props

  • 优点

    1. 简单直接,可读性强。
    2. React 官方推荐的常见做法,易于维护。
  • 示例

jsx
// 父组件
function Parent() {
  const [name, setName] = React.useState('Tom');

  return (
    <div>
      <Child userName={name} />
    </div>
  );
}

// 子组件
function Child(props) {
  return <div>我的名字是:{props.userName}</div>;
}

在上述示例中,Parent 组件通过 userName 这个属性将数据传递给 Child 组件,子组件则通过 props.userName 获取对应的值。


2. 子组件向父组件通信

2.1 通过回调函数(Props 回调)

  • 使用场景
    如果子组件需要告诉父组件某件事情(比如事件触发的结果、某些内部状态的变化等),我们通常会在父组件中定义一个回调函数,通过 props 传递给子组件。子组件在合适的时机调用该回调函数,并将需要的信息当作参数传回给父组件。

  • 示例

    jsx
    // 父组件
    function Parent() {
      const [count, setCount] = React.useState(0);
    
      const handleChildUpdate = (newValue) => {
        setCount(newValue);
      };
    
      return (
        <div>
          <p>父组件接收到:{count}</p>
          <Child onUpdate={handleChildUpdate} />
        </div>
      );
    }
    
    // 子组件
    function Child(props) {
      const handleClick = () => {
        // 假设我们希望把随机生成的数字传给父组件
        const randomValue = Math.floor(Math.random() * 100);
        props.onUpdate(randomValue);
      };
    
      return (
        <button onClick={handleClick}>
          点击更新父组件
        </button>
      );
    }

当子组件执行 handleClick 中的 props.onUpdate 时,父组件 Parent 会接收到 newValue,更新自己的状态。


3. 跨级组件通信

当需要在层级结构中向深层次的子组件传递数据或函数时,如果只用“自上而下”逐层传递 props,会非常繁琐且冗长。对于跨级的组件通信,可以考虑以下方案:

3.1 React Context

  • 使用场景
    对于需要跨多个层级传递的静态或较少变化的数据(例如主题、语言、登录信息等),可以使用 React.createContext()Context.Provider

  • 使用方法

    1. 创建 Context:const MyContext = React.createContext(defaultValue);
    2. 在顶层组件使用 MyContext.Provider 提供数据。
    3. 在需要使用数据的深层组件中,通过 useContext(MyContext) 或者 <MyContext.Consumer> 读取数据。
  • 示例

jsx
// 创建 Context
const ThemeContext = React.createContext('light');

// 顶层组件
function App() {
  const [theme, setTheme] = React.useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 中间层组件
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// 深层组件
function ThemedButton() {
  const theme = React.useContext(ThemeContext);
  return <button className={theme}>按钮</button>;
}

3.2 Redux / MobX / Zustand 等状态管理库

  • 使用场景
    1. 应用规模较大,需要更全面或集中式的状态管理。
    2. 数据需要被多个不相关或跨层级组件频繁地读取或更新。
  • 思路
    将组件需要共享的状态放到一个“公共”Store 中,组件通过订阅(subscribe)或使用 hooks(如 useSelector, useDispatch)读取和更新 Store 中的数据。

3.3 其他方式:自定义 Context + 自己维护事件或数据

对于简单场景,也可以自己创建一个“事件中心”或“数据中心”,在 Provider 中提供给所有需要它的子组件,类似于手写一个简化版的 Redux。


4. 非嵌套关系的组件通信

有时两个组件之间不存在直接的父子层级关系,或者层级关系非常远,甚至不同页面下的组件需要通信。这类情况可以考虑以下方案:

4.1 提升(Hoist)状态到最近的公共父组件

  • 思路
    找到这两个组件最近的公共父组件,将共同使用的状态提升到这个父组件中,然后以 props 形式将数据和回调分发给子组件。这样它们可以共享同一个状态源,并进行通信。

  • 示例
    假设 ComponentAComponentB 需要共享一些信息,二者都在同一个 Parent 下(或者有一个公共祖先 GrandParent)。则将状态定义在公共祖先组件,然后分发下去。

4.2 全局状态管理 / Redux / Context 等

  • 使用场景
    如果要通信的组件非常分散或者彼此间没有明显的公共父组件,使用 Context 或者 Redux 这样的全局状态管理库是一种更灵活的方案。

4.3 自定义事件总线(Event Emitter)

  • 使用场景
    在一些小型项目中,如果仅仅需要在少数组件间订阅/发布事件,可以使用简单的事件总线模式:

    1. 创建一个单例的事件管理器(如 EventEmitter 实例)。
    2. 需要发送消息的组件通过 eventEmitter.emit() 发布事件。
    3. 需要接收消息的组件通过 eventEmitter.on() 订阅事件。
  • 注意事项

    1. 要记得在组件卸载时取消订阅,避免内存泄漏。
    2. 事件总线模式在大型应用中容易出现维护成本高、事件难以追踪等问题,需慎重使用。

事件总线

在前端或其他编程场景中,事件总线(Event Bus) 是一种用于解耦组件之间通信的机制,常常可以类比为“一个公共的事件中心”。它通常基于发布-订阅(Publish-Subscribe) 模式或观察者(Observer) 模式:

  • 发布(Publish / Emit):需要广播消息的组件向事件总线发送事件(如 eventBus.emit('eventName', data))。
  • 订阅(Subscribe / On):需要监听某事件的组件向事件总线注册回调(如 eventBus.on('eventName', callback))。

一旦“发布”动作发生,所有订阅了该事件的回调函数就会被触发,从而实现了组件间的解耦通信

  • 组件 A 只管“发出”事件,而不必关心谁来“接收”事件;
  • 组件 B 只管“监听”事件,而不必关心是谁发出的。

这样,在一些没有明显层级关系或多层级嵌套较深、需要通信的组件之间,我们可以通过事件总线把它们“连接”起来,而无需相互引用或在层级间层层传递 props。


1. 基本实现思路

在 JavaScript 中,实现一个事件总线通常只需要一个对象,用来维护事件名与回调函数列表之间的映射关系。例如:

js
class EventBus {
  constructor() {
    this.events = {};
  }

  on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }

  emit(eventName, ...args) {
    const handlers = this.events[eventName];
    if (handlers) {
      handlers.forEach((cb) => cb(...args));
    }
  }

  off(eventName, callback) {
    const handlers = this.events[eventName];
    if (handlers) {
      // 移除对应的回调函数
      this.events[eventName] = handlers.filter(cb => cb !== callback);
    }
  }
}

// 创建一个全局单例
export const eventBus = new EventBus();

然后在需要发布事件的组件中:

jsx
import { eventBus } from './EventBus';

function Publisher() {
  const handleClick = () => {
    eventBus.emit('loginSuccess', { user: 'Tom' });
  };

  return <button onClick={handleClick}>Login</button>;
}

在需要监听事件的组件中:

jsx
import { eventBus } from './EventBus';
import { useEffect } from 'react';

function Subscriber() {
  useEffect(() => {
    const callback = (data) => {
      console.log('Received loginSuccess event: ', data);
    };
    eventBus.on('loginSuccess', callback);

    return () => {
      // 组件卸载时取消订阅,防止内存泄漏
      eventBus.off('loginSuccess', callback);
    };
  }, []);

  return <div>我是监听登录成功事件的组件</div>;
}

2. 适用场景与优点

  1. 非父子关系组件通信
    对于没有明显层级的组件之间(如页面上各个相互独立的部分)的通信,通过事件总线能避免“组件 A 引用组件 B”的强耦合。
  2. 松耦合设计
    组件只需关注要发送什么事件或监听什么事件,而不需关心谁会处理、或者事件来自哪里。
  3. 快速原型/小型项目
    在一些对架构要求不高的小型项目中,使用事件总线可以快速实现组件通信,而不必上手 Redux、Context 等较重的状态管理方案。

3. 常见的局限与注意事项

  1. 可维护性
    事件总线容易导致事件满天飞,如果事件名定义不清晰或数量过多,就会难以维护与追踪,很容易出现调试困难。
  2. 数据流不透明
    事件触发的结果往往是分散的,可能某个事件会触发多个订阅者执行逻辑,也可能其中有些组件不小心忘记取消订阅。
  3. 内存泄漏
    组件卸载后若不及时移除对应的事件订阅,事件总线依旧持有它们的回调引用,造成内存无法被释放。
  4. 与 React Hooks 的配合
    使用事件总线时,更要注意在 useEffect 或其他合适的生命周期中进行订阅与取消订阅,避免重复绑定或遗漏解绑。

评论区

欢迎留言、补充或勘误。

xiaoba.blog