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

news2025/1/2 9:51:13

React 组件通信完整指南

1. 父子组件通信

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

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

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

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

// 父组件
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 父组件调用子组件方法

// 父组件
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 通过共同父组件

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

// 创建 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
  • 接受消息的组件订阅消息
  • 提供数据的组件发布消息
  • 可在兄弟组件,祖孙组件进行通讯
基本用法示例
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 自定义事件发布订阅系统

// 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 类

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

EventBus 是一个类,里面有一个 events 对象,用来存储所有事件及其对应的回调函数。
this.events 以事件名为键 (key),回调函数数组为值 (value),用来存储订阅的事件和回调函数。

3.2.2. subscribe 方法

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] 数组中移除给定的回调函数来实现的。
订阅示例:


const unsubscribe = eventBus.subscribe('someEvent', (data) => {
  console.log(data);
});

这样,当 ‘someEvent’ 事件发生时,回调会执行。
调用 unsubscribe() 可以取消订阅该事件的回调。

3.2.3. publish 方法

publish(event, data) {
  if (this.events[event]) {
    this.events[event].forEach(callback => callback(data));
  }
}

publish 方法用于触发某个事件 (event),并向订阅该事件的回调函数传递数据 (data)。
如果事件在 this.events 中存在,它会依次执行所有与该事件相关的回调函数,并把 data 作为参数传递给回调函数。
发布事件示例:

eventBus.publish('someEvent', { key: 'value' });

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

3.2.4. 实例化 EventBus

export default new EventBus();

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

3.2.5 总结

订阅事件:通过 subscribe 方法,可以为某个事件注册一个回调函数。
发布事件:通过 publish 方法,可以触发某个事件,并将数据传递给所有已订阅该事件的回调函数。
取消订阅:subscribe 返回的函数可以用来取消订阅某个事件。

// 使用自定义事件系统
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

// 安装: 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 性能优化

// 使用 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. 清理订阅
useEffect(() => {
  const subscription = someEventSource.subscribe();
  return () => subscription.unsubscribe();
}, []);
  1. 避免过度使用全局状态
// 不推荐
const GlobalContext = React.createContext();

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

5. 总结

组件通信方式选择建议:

  1. 就近原则

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

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

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

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

实际应用场景

  1. 跨组件通信:
// 数据更新组件(发布者)
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. 全局状态变化通知:
// 主题切换组件(发布者)
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. 命名约定:
// 使用常量定义主题名称
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. 性能考虑:
function OptimizedComponent() {
  useEffect(() => {
    // 使用防抖或节流处理高频事件
    const handleDataChange = debounce((topic, data) => {
      // 处理数据变化
    }, 200);

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2267991.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

科大讯飞超拟人合成python

1、进入自己的项目 复制APPID、APISecret、APIKey 2、添加好听发音人 复制参数 3、需要替换代码部分&#xff1a; 换自己喜欢的发声人的参数 4、完整代码&#xff1a; import _thread as thread import base64 import datetime import hashlib import hmac import json fro…

关于缓冲文件系统和文件控制块的介绍

缓冲文件系统 缓冲文件系统的定义与原理 应用程序是如何进行文件数据的访问的呢&#xff1f;由于系统对磁盘文件数据的存取速度与内存数据存取的速度不同&#xff0c;而且文件数据量较大&#xff0c;数据从磁盘读到内存或从内存写到磁盘不可能瞬间完成&#xff0c;所以为了提高…

Llama系列关键知识总结

系列文章目录 第一章&#xff1a;LoRA微调系列笔记 第二章&#xff1a;Llama系列关键知识总结 文章目录 系列文章目录Llama: Open and Efficient Foundation Language Models关键要点LLaMa模型架构&#xff1a;Llama2分组查询注意力 (GQA) Llama3关键信息 引用&#xff1a; Ll…

【已解决】Latex中高亮段内命令(如参考文献引用、图、表格)

速览&#xff1a;解决前后图片对比拟解决的问题问题描述Latex高亮的一般做法段内有命令时候的高亮报错 问题原因 解决方案——在导言区为 \cite 等命令“注册”解决方案简要描述详细解释其他情况 速览&#xff1a;解决前后图片对比 解决前&#xff1a; 解决后&#xff1a; …

【C语言】数组指针与指针数组

前言 前面的文章讲了指针的一些基本内容&#xff0c;这里我们来讲一下数组指针与指针数组&#xff0c;数组指针是指针运用的一个明显体现&#xff0c;准确来说是通过指针访问内存地址的具体体现 一、一维数组的指针 首先&#xff0c;我们先来看一段代码 #include <stdio…

30天面试打卡计划 2024-12-25 26 27 面试题

2024-12-25 面试题 后端 MySQL三层B树能存多少数据&#xff1f; B 树&#xff1a;一种特殊的多路平衡查找树&#xff0c;广泛应用于数据库索引中。它具有所有叶子节点都位于同一层且包含指向相邻叶子节点指针的特点&#xff0c;这使得范围查询更加高效。InnoDB&#xff1a;My…

嵌入式系统 第十一讲 Android操作系统(增加)

• 11.1 Android 操作系统介绍 • Android 是 Google 公司于2007 年11月发布的一款非常优秀的智能移 动平台操作系统。到2011 年第一季度Android 在全球的市场份额首 次超过Nokia的Symbian系统&#xff0c;跃居全球第一。 • Android系统最初由AndyRubin等人于2003年10月创建…

Three.js 字体

在 Three.js 中&#xff0c;我们可以通过 FontLoader 加载字体&#xff0c;并结合 TextGeometry 创建 3D 文本。加载字体是因为字体文件包含了字体的几何信息&#xff0c;例如字体的形状、大小、粗细等&#xff0c;而 TextGeometry 则是根据字体信息生成 3D 文本的几何体。 在…

机器人C++开源库The Robotics Library (RL)使用手册(三)

进入VS工程,我们先看看这些功能函数及其依赖库的分布关系: rl命名空间下,主要有八大模块。 搞定VS后将逐个拆解。 1、编译运行 根据报错提示,配置相应错误的库(根据每个人安装位置不同而不同,我的路径如下:) 编译所有,Release版本耗时大约10分钟。 以rlPlan运动…

【GUI-PyQt5】简介

1. 简介 GUI&#xff1a;带图形的用户接口程序&#xff0c;也就是桌面应用。 2. 分类 2.1 基本窗口控件 QMainWindowQwidgetQlabelQLineEdit菜单工具栏 2.2 高级组件 QTableViewQListView容器多线程 2.3 布局管理 QBoxLayoutQGridLayoutQFormLayout嵌套布局 2.4 信号与…

Mysql学习笔记之SQL-4

这篇文章开始介绍SQL语句的最后一个部分&#xff0c;DCL&#xff08;Data Control Language&#xff09;数据库控制语言。 1.简介 DCL英文全称是Data Control Language(数据控制语言)&#xff0c;用来管理数据库用户、控制数据库的访 问权限。 这一部分比较简单&#xff0c;主…

Chrome被360导航篡改了怎么改回来?

一、Chrome被360导航篡改了怎么改回来&#xff1f; 查看是否被360主页锁定&#xff0c;地址栏输入chrome://version&#xff0c;看命令行end后面&#xff08;蓝色部分&#xff09;&#xff0c;是否有https://hao.360.com/?srclm&lsn31c42a959f 修改步骤 第一步&#xff1a…

STM32-笔记18-呼吸灯

1、实验目的 使用定时器 4 通道 3 生成 PWM 波控制 LED1 &#xff0c;实现呼吸灯效果。 频率&#xff1a;2kHz&#xff0c;PSC71&#xff0c;ARR499 利用定时器溢出公式 周期等于频率的倒数。故Tout 1/2KHZ&#xff1b;Ft 72MHZ PSC71&#xff08;喜欢设置成Ft的倍数&…

内部类(2)

大家还&#xff0c;今天我们继续来学习内部类的知识&#xff0c;今天我们来看看其余几种内部类类型&#xff0c;那么话不多说 我们直接开始。 注&#xff1a;它是一个static的一个常量 一旦初始化就不能够进行修改了. 注:1.一般情况下我们定义常量的时候,会定成大写的: 2.a不…

Go Energy 跨平台框架 v2.5.1 发布

Energy 框架 是Go语言基于CEF 和 LCL 开发的跨平台 GUI 框架, 具体丰富的系统原生 UI 控件集, 丰富的 CEF 功能 API&#xff0c;简化且不失功能的 CEF 功能 API 使用。 特性&#xff1f; 特性描述跨平台支持 Windows, macOS, Linux简单Go语言的简单特性&#xff0c;使用简单…

欧科云链OKLink:比特币与以太坊“双重启动”将如何撬动市场?

近日&#xff0c;OKLink 与 137Labs 联合举办 X Space&#xff0c;围绕宏观经济环境、政策及机构投资的影响等话题&#xff0c;分享如何把握 Web3 中的潜在机会与辨别风险。OKG Research 首席研究员 Hedy、BuilderRocket Accelerator 研究合伙人 Vivienna、VC 分析员 Bunny、BU…

探索仓颉编程语言:功能、实战与展望

目录 引言 一.使用体验 二.功能剖析 1.丰富的数据类型与控制结构 2.强大的编程范式支持 3.标准库与模块系统 4.并发编程能力 三.实战案例 1.项目背景与目标 2.具体实现步骤 (1).导入必要的模块 (2).发送 HTTP 请求获取网页内容 (3).解析 HTML 页面提取文章信息 (…

JavaFX FXML模式下的布局

常见布局方式概述 在 JavaFX FXML 模式下&#xff0c;有多种布局方式可供选择。这些布局方式可以帮助您有效地组织和排列 UI 组件&#xff0c;以创建出美观且功能良好的用户界面。常用布局容器及布局方式 BorderPane 布局 特点&#xff1a;BorderPane 将空间划分为五个区域&…

OpenFeign介绍以及使用

介绍 OpenFeign 是一个声明式的 Web 服务客户端&#xff0c;用于简化在 Java 应用中调用 HTTP API 的过程&#xff0c;在 Spring Cloud 体系里被广泛应用&#xff0c;它有以下关键特性&#xff1a; 声明式调用&#xff1a;基于注解&#xff0c;开发人员只需定义接口并添加注解…

李永乐线性代数:A可逆,AX=B相关推论和例题解题思路

例题1&#xff1a; 思路讲解&#xff1a; 这个 (A-2E)可逆,所以有P(A-2E) E&#xff0c; 也就是(A-2E)的逆矩阵是P&#xff1b; 那么PA (A-2E)的逆 * A B P(A-2E,A)(E,B) 所以就可以直接求出B&#xff0c;也就是(A-2E)的逆 * A 例题2&#xff1a; 思路讲解&#xff1a;…