React 组件通信完整指南 以及 自定义事件发布订阅系统

React 组件通信完整指南

1. 父子组件通信

1.1 父组件向子组件传递数据

jsx 复制代码
// 父组件
function ParentComponent() {
  const [data, setData] = useState('Hello from parent');
  
  return <ChildComponent message={data} />;
}

// 子组件
function ChildComponent({ message }) {
  return <div>{message}</div>;
}

1.2 子组件向父组件传递数据

jsx 复制代码
// 父组件
function ParentComponent() {
  const handleChildData = (data) => {
    console.log('Received from child:', data);
  };
  
  return <ChildComponent onDataSend={handleChildData} />;
}

// 子组件
function ChildComponent({ onDataSend }) {
  const sendData = () => {
    onDataSend('Hello from child');
  };
  
  return <button onClick={sendData}>Send Data to Parent</button>;
}

1.3 父组件调用子组件方法

jsx 复制代码
// 父组件
function ParentComponent() {
  const childRef = useRef();
  
  const handleClick = () => {
    childRef.current.childMethod();
  };
  
  return (
    <div>
      <ChildComponent ref={childRef} />
      <button onClick={handleClick}>Call Child Method</button>
    </div>
  );
}

// 子组件
const ChildComponent = forwardRef((props, ref) => {
  useImperativeHandle(ref, () => ({
    childMethod: () => {
      console.log('Child method called');
    }
  }));
  
  return <div>Child Component</div>;
});

2. 兄弟组件通信

2.1 通过共同父组件

jsx 复制代码
function ParentComponent() {
  const [sharedData, setSharedData] = useState('');
  
  return (
    <div>
      <SiblingOne onDataChange={setSharedData} />
      <SiblingTwo data={sharedData} />
    </div>
  );
}

function SiblingOne({ onDataChange }) {
  return (
    <button onClick={() => onDataChange('Hello from Sibling One')}>
      Send to Sibling
    </button>
  );
}

function SiblingTwo({ data }) {
  return <div>Received: {data}</div>;
}

2.2 使用 Context

jsx 复制代码
// 创建 Context
const DataContext = React.createContext();

// 父组件提供 Context
function ParentComponent() {
  const [sharedData, setSharedData] = useState('');
  
  return (
    <DataContext.Provider value={{ data: sharedData, setData: setSharedData }}>
      <SiblingOne />
      <SiblingTwo />
    </DataContext.Provider>
  );
}

// 兄弟组件一
function SiblingOne() {
  const { setData } = useContext(DataContext);
  
  return (
    <button onClick={() => setData('Hello from Context')}>
      Update Context
    </button>
  );
}

// 兄弟组件二
function SiblingTwo() {
  const { data } = useContext(DataContext);
  
  return <div>Context Data: {data}</div>;
}

3. 消息订阅与发布

3.1 使用 PubSubJS

PubSubJS 是一个基于主题的发布/订阅库。

  • 官方文档: https://github.com/mroderick/PubSubJS
  • 安装: npm install pubsub-js
  • 接受消息的组件订阅消息
  • 提供数据的组件发布消息
  • 可在兄弟组件,祖孙组件进行通讯
基本用法示例
jsx 复制代码
import PubSub from 'pubsub-js';

// 定义消息主题
const TOPICS = {
  USER_LOGGED_IN: 'USER_LOGGED_IN',
  DATA_UPDATED: 'DATA_UPDATED',
  NOTIFICATION: 'NOTIFICATION'
};

// 登录组件(发布者)
function LoginComponent() {
  const handleLogin = () => {
    // 登录成功后发布消息
    PubSub.publish(TOPICS.USER_LOGGED_IN, {
      userId: '123',
      username: 'john_doe',
      timestamp: new Date()
    });
  };

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

// 头部组件(订阅者)
function HeaderComponent() {
  const [username, setUsername] = useState('');

  useEffect(() => {
    // 订阅登录消息
    const token = PubSub.subscribe(TOPICS.USER_LOGGED_IN, (topic, data) => {
      setUsername(data.username);
      console.log(`User ${data.username} logged in at ${data.timestamp}`);
    });

    return () => {
      // 组件卸载时取消订阅
      PubSub.unsubscribe(token);
    };
  }, []);

  return <div>Welcome, {username}</div>;
}

// 通知组件(订阅者)
function NotificationComponent() {
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    // 可以同时订阅多个主题
    const tokens = [
      PubSub.subscribe(TOPICS.USER_LOGGED_IN, (topic, data) => {
        setNotifications(prev => [...prev, `New login: ${data.username}`]);
      }),
      PubSub.subscribe(TOPICS.DATA_UPDATED, (topic, data) => {
        setNotifications(prev => [...prev, `Data updated: ${data.message}`]);
      })
    ];

    return () => {
      // 清理所有订阅
      tokens.forEach(token => PubSub.unsubscribe(token));
    };
  }, []);

  return (
    <div>
      <h3>Notifications</h3>
      <ul>
        {notifications.map((note, index) => (
          <li key={index}>{note}</li>
        ))}
      </ul>
    </div>
  );
}

3.2 自定义事件发布订阅系统

javascript 复制代码
// eventBus.js
class EventBus {
  constructor() {
    this.events = {};
  }
  
  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
    
    return () => {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    };
  }
  
  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

export default new EventBus();

3.2.1. EventBus 类

javascript 复制代码
class EventBus {
  constructor() {
    this.events = {};
  }

EventBus 是一个类,里面有一个 events 对象,用来存储所有事件及其对应的回调函数。

this.events 以事件名为键 (key),回调函数数组为值 (value),用来存储订阅的事件和回调函数。

3.2.2. subscribe 方法

javascript 复制代码
subscribe(event, callback) {
  if (!this.events[event]) {
    this.events[event] = [];
  }
  this.events[event].push(callback);
  
  return () => {
    this.events[event] = this.events[event].filter(cb => cb !== callback);
  };
}

subscribe 方法用于订阅某个事件 (event) 并提供一个回调函数 (callback)。

如果事件名 event 不存在于 this.events 中,会初始化为一个空数组。

然后把回调函数添加到事件对应的回调函数数组中。

subscribe 方法返回一个取消订阅的函数。这是通过在返回值中使用 filter 方法,从 this.events[event] 数组中移除给定的回调函数来实现的。

订阅示例:

javascript 复制代码
const unsubscribe = eventBus.subscribe('someEvent', (data) => {
  console.log(data);
});

这样,当 'someEvent' 事件发生时,回调会执行。

调用 unsubscribe() 可以取消订阅该事件的回调。

3.2.3. publish 方法

javascript 复制代码
publish(event, data) {
  if (this.events[event]) {
    this.events[event].forEach(callback => callback(data));
  }
}

publish 方法用于触发某个事件 (event),并向订阅该事件的回调函数传递数据 (data)。

如果事件在 this.events 中存在,它会依次执行所有与该事件相关的回调函数,并把 data 作为参数传递给回调函数。

发布事件示例:

javascript 复制代码
eventBus.publish('someEvent', { key: 'value' });

这会触发所有订阅 'someEvent' 的回调,并将 { key: 'value' } 传递给它们。

3.2.4. 实例化 EventBus

javascript 复制代码
export default new EventBus();

这一行创建了一个 EventBus 的实例,并将其导出。这样其他模块就可以直接使用这个实例来订阅和发布事件,而无需每次都创建新的 EventBus 实例。

3.2.5 总结

订阅事件:通过 subscribe 方法,可以为某个事件注册一个回调函数。

发布事件:通过 publish 方法,可以触发某个事件,并将数据传递给所有已订阅该事件的回调函数。

取消订阅:subscribe 返回的函数可以用来取消订阅某个事件。

jsx 复制代码
// 使用自定义事件系统
import eventBus from './eventBus';

// 发布者组件
function Publisher() {
  const publishEvent = () => {
    eventBus.publish('customEvent', {
      message: 'Hello from custom event'
    });
  };
  
  return <button onClick={publishEvent}>Publish Event</button>;
}

// 订阅者组件
function Subscriber() {
  const [message, setMessage] = useState('');
  
  useEffect(() => {
    const unsubscribe = eventBus.subscribe('customEvent', (data) => {
      setMessage(data.message);
    });
    
    return () => unsubscribe();
  }, []);
  
  return <div>Custom Event Message: {message}</div>;
}

3.3 使用 RxJS

jsx 复制代码
// 安装: npm install rxjs

import { Subject } from 'rxjs';

const messageSubject = new Subject();

// 发布者组件
function RxPublisher() {
  const publishMessage = () => {
    messageSubject.next({
      text: 'Hello from RxJS',
      timestamp: new Date()
    });
  };
  
  return <button onClick={publishMessage}>Publish RxJS Message</button>;
}

// 订阅者组件
function RxSubscriber() {
  const [message, setMessage] = useState('');
  
  useEffect(() => {
    const subscription = messageSubject.subscribe(data => {
      setMessage(data.text);
    });
    
    return () => subscription.unsubscribe();
  }, []);
  
  return <div>RxJS Message: {message}</div>;
}

4. 最佳实践

4.1 选择合适的通信方式

  1. 父子组件通信

    • 优先使用 props 和回调函数
    • 需要调用子组件方法时使用 ref
  2. 兄弟组件通信

    • 简单场景:通过共同父组件
    • 复杂场景:使用 Context 或状态管理库
  3. 跨层级组件通信

    • 使用 Context
    • 使用消息订阅发布
    • 考虑使用状态管理库(Redux/MobX)

4.2 性能优化

jsx 复制代码
// 使用 useMemo 优化 props
function ParentComponent() {
  const [count, setCount] = useState(0);
  
  const expensiveData = useMemo(() => {
    return computeExpensiveValue(count);
  }, [count]);
  
  return <ChildComponent data={expensiveData} />;
}

// 使用 useCallback 优化回调函数
function ParentComponent() {
  const handleClick = useCallback((value) => {
    console.log(value);
  }, []);
  
  return <ChildComponent onClick={handleClick} />;
}

4.3 注意事项

  1. 清理订阅
jsx 复制代码
useEffect(() => {
  const subscription = someEventSource.subscribe();
  return () => subscription.unsubscribe();
}, []);
  1. 避免过度使用全局状态
jsx 复制代码
// 不推荐
const GlobalContext = React.createContext();

// 推荐:将 Context 拆分为更小的粒度
const UserContext = React.createContext();
const ThemeContext = React.createContext();
  1. 合理使用 memo
jsx 复制代码
const MemoizedChild = React.memo(ChildComponent, (prevProps, nextProps) => {
  return prevProps.value === nextProps.value;
});

5. 总结

组件通信方式选择建议:

  1. 就近原则

    • 父子组件优先使用 props
    • 兄弟组件优先通过父组件通信
  2. 灵活性考虑

    • 简单场景使用 props 和回调
    • 复杂场景考虑发布订阅或状态管理
  3. 性能考虑

    • 合理使用 useMemo 和 useCallback
    • 适当使用 React.memo
    • 注意清理订阅避免内存泄漏
  4. 维护性考虑

    • 保持通信逻辑清晰
    • 避免过度使用全局状态
    • 合理划分组件职责

实际应用场景

  1. 跨组件通信:
javascript 复制代码
// 数据更新组件(发布者)
function DataUpdateComponent() {
  const updateData = () => {
    // 执行数据更新操作
    PubSub.publish(TOPICS.DATA_UPDATED, {
      message: 'Data has been updated',
      timestamp: new Date()
    });
  };

  return <button onClick={updateData}>Update Data</button>;
}

// 多个需要响应数据更新的组件(订阅者)
function TableComponent() {
  const [data, setData] = useState([]);

  useEffect(() => {
    const token = PubSub.subscribe(TOPICS.DATA_UPDATED, () => {
      // 重新获取数据
      fetchData().then(setData);
    });

    return () => PubSub.unsubscribe(token);
  }, []);

  return <table>{/* 渲染数据 */}</table>;
}

function ChartComponent() {
  const [chartData, setChartData] = useState(null);

  useEffect(() => {
    const token = PubSub.subscribe(TOPICS.DATA_UPDATED, () => {
      // 更新图表数据
      updateChartData();
    });

    return () => PubSub.unsubscribe(token);
  }, []);

  return <div>{/* 渲染图表 */}</div>;
}
  1. 全局状态变化通知:
jsx 复制代码
// 主题切换组件(发布者)
function ThemeToggle() {
  const toggleTheme = () => {
    const newTheme = 'dark';
    PubSub.publish('THEME_CHANGED', { theme: newTheme });
  };

  return <button onClick={toggleTheme}>Toggle Theme</button>;
}

// 需要响应主题变化的组件(订阅者)
function ThemedComponent() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const token = PubSub.subscribe('THEME_CHANGED', (_, data) => {
      setTheme(data.theme);
      // 更新组件样式
    });

    return () => PubSub.unsubscribe(token);
  }, []);

  return <div className={theme}>{/* 组件内容 */}</div>;
}

3.2 使用注意事项

  1. 命名约定:
javascript 复制代码
// 使用常量定义主题名称
const TOPICS = {
  USER_ACTION: 'USER_ACTION',
  SYSTEM_EVENT: 'SYSTEM_EVENT',
  DATA_CHANGE: 'DATA_CHANGE'
};

// 使用命名空间避免冲突
const TOPICS = {
  USER: {
    LOGIN: 'USER.LOGIN',
    LOGOUT: 'USER.LOGOUT'
  },
  DATA: {
    UPDATE: 'DATA.UPDATE',
    DELETE: 'DATA.DELETE'
  }
};
  1. 性能考虑:
jsx 复制代码
function OptimizedComponent() {
  useEffect(() => {
    // 使用防抖或节流处理高频事件
    const handleDataChange = debounce((topic, data) => {
      // 处理数据变化
    }, 200);

    const token = PubSub.subscribe('DATA_CHANGE', handleDataChange);
    return () => PubSub.unsubscribe(token);
  }, []);
}
  1. 错误处理:
jsx 复制代码
function RobustSubscriber() {
  useEffect(() => {
    const token = PubSub.subscribe('TOPIC', (topic, data) => {
      try {
        // 处理数据
      } catch (error) {
        console.error('Error handling published data:', error);
        // 错误处理逻辑
      }
    });

    return () => PubSub.unsubscribe(token);
  }, []);
}
相关推荐
蜗牛快跑2133 小时前
前端正在被“锈”化
前端·代码规范
Jet_closer_burning5 小时前
微信小程序中遇到过的问题
前端·微信小程序·小程序
掘金酱6 小时前
稀土掘金社区2024年度影响力榜单正式公布
android·前端·后端
Keven__Java6 小时前
Java开发-后端请求成功,前端显示失败
java·开发语言·前端
轻口味6 小时前
【每日学点鸿蒙知识】渐变效果、Web组件注册对象报错、深拷贝list、loadContent数据共享、半屏弹窗
前端·list·harmonyos
老K(郭云开)6 小时前
最新版Chrome浏览器加载ActiveX控件技术——alWebPlugin中间件V2.0.28-迎春版发布
前端·chrome·中间件
轻口味6 小时前
【每日学点鸿蒙知识】子窗口方向、RichEdit不居中、本地资源缓存给web、Json转对象丢失方法、监听状态变量数组中内容改变
前端·缓存·harmonyos
我是苏苏6 小时前
Web开发:ORM框架之使用Freesql的分表分页写法
前端·数据库·sql
m0_674031437 小时前
React - useContext和深层传递参数
前端·javascript·react.js
傻小胖7 小时前
React Diffing 算法完整指南
开发语言·react.js