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 选择合适的通信方式
-
父子组件通信:
- 优先使用 props 和回调函数
- 需要调用子组件方法时使用 ref
-
兄弟组件通信:
- 简单场景:通过共同父组件
- 复杂场景:使用 Context 或状态管理库
-
跨层级组件通信:
- 使用 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 注意事项
- 清理订阅
jsx
useEffect(() => {
const subscription = someEventSource.subscribe();
return () => subscription.unsubscribe();
}, []);
- 避免过度使用全局状态
jsx
// 不推荐
const GlobalContext = React.createContext();
// 推荐:将 Context 拆分为更小的粒度
const UserContext = React.createContext();
const ThemeContext = React.createContext();
- 合理使用 memo
jsx
const MemoizedChild = React.memo(ChildComponent, (prevProps, nextProps) => {
return prevProps.value === nextProps.value;
});
5. 总结
组件通信方式选择建议:
-
就近原则:
- 父子组件优先使用 props
- 兄弟组件优先通过父组件通信
-
灵活性考虑:
- 简单场景使用 props 和回调
- 复杂场景考虑发布订阅或状态管理
-
性能考虑:
- 合理使用 useMemo 和 useCallback
- 适当使用 React.memo
- 注意清理订阅避免内存泄漏
-
维护性考虑:
- 保持通信逻辑清晰
- 避免过度使用全局状态
- 合理划分组件职责
实际应用场景
- 跨组件通信:
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>;
}
- 全局状态变化通知:
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 使用注意事项
- 命名约定:
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'
}
};
- 性能考虑:
jsx
function OptimizedComponent() {
useEffect(() => {
// 使用防抖或节流处理高频事件
const handleDataChange = debounce((topic, data) => {
// 处理数据变化
}, 200);
const token = PubSub.subscribe('DATA_CHANGE', handleDataChange);
return () => PubSub.unsubscribe(token);
}, []);
}
- 错误处理:
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);
}, []);
}