React 组件之间如何通信?
问题解析
面试官考察点:
- 对 React 数据流的理解
- 掌握不同场景下的通信方案
- 能否根据实际场景选择合适的通信方式
- 对 React 生态工具的了解
核心概念
React 遵循单向数据流的设计思想,数据主要从父组件流向子组件。组件间通信即指组件通过某种方式来传递信息以达到某个目的。
通信方式分类
根据组件关系的不同,通信方式可以分为:
- 父子组件通信
- 兄弟组件通信
- 跨层级组件通信
- 非关系组件通信
详细解答
1. 父组件向子组件传递(Props)
// 父组件
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<Child count={count} message="Hello from parent" />
</div>
);
}
// 子组件
function Child({ count, message }) {
return (
<div>
<p>Count: {count}</p>
<p>Message: {message}</p>
</div>
);
}
2. 子组件向父组件传递(回调函数)
// 父组件
function Parent() {
const [value, setValue] = useState('');
const handleChildData = (data) => {
setValue(data);
console.log('Received from child:', data);
};
return (
<div>
<p>Value from child: {value}</p>
<Child onSendData={handleChildData} />
</div>
);
}
// 子组件
function Child({ onSendData }) {
const [inputValue, setInputValue] = useState('');
const handleClick = () => {
onSendData(inputValue);
};
return (
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<button onClick={handleClick}>Send to Parent</button>
</div>
);
}
3. 兄弟组件之间的通信(状态提升)
// 父组件作为中间层
function Parent() {
const [sharedData, setSharedData] = useState('');
return (
<div>
<SiblingA data={sharedData} />
<SiblingB onDataChange={setSharedData} />
</div>
);
}
// 兄弟组件 A - 接收数据
function SiblingA({ data }) {
return <div>Received: {data}</div>;
}
// 兄弟组件 B - 发送数据
function SiblingB({ onDataChange }) {
return (
<button onClick={() => onDataChange('Hello from B')}>
Send Data
</button>
);
}
4. 跨层级组件通信(Context)
// 创建 Context
const ThemeContext = React.createContext('light');
// 祖先组件 - 提供数据
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 中间组件
function Toolbar() {
return <ThemedButton />;
}
// 深层组件 - 消费数据(方式1:useContext)
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
style={{ background: theme === 'dark' ? '#333' : '#fff' }}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
Current theme: {theme}
</button>
);
}
// 深层组件 - 消费数据(方式2:Consumer)
function ThemedButton2() {
return (
<ThemeContext.Consumer>
{({ theme, setTheme }) => (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme}
</button>
)}
</ThemeContext.Consumer>
);
}
5. 非关系组件通信(全局状态管理)
Redux 示例
// store.js
import { createStore } from 'redux';
const counterReducer = (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
};
const store = createStore(counterReducer);
// 组件 A - 派发 action
function ComponentA() {
const increment = () => {
store.dispatch({ type: 'INCREMENT' });
};
return <button onClick={increment}>Increment</button>;
}
// 组件 B - 订阅 state
function ComponentB() {
const [count, setCount] = useState(store.getState().count);
useEffect(() => {
const unsubscribe = store.subscribe(() => {
setCount(store.getState().count);
});
return unsubscribe;
}, []);
return <div>Count: {count}</div>;
}
使用 react-redux
// 连接 Redux
import { connect } from 'react-redux';
// 展示组件
function Counter({ count, increment }) {
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
// 容器组件
const mapStateToProps = (state) => ({
count: state.count
});
const mapDispatchToProps = (dispatch) => ({
increment: () => dispatch({ type: 'INCREMENT' })
});
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
6. 使用 Ref 进行组件通信
// 父组件获取子组件实例
class Parent extends React.Component {
childRef = React.createRef();
handleClick = () => {
// 调用子组件方法
this.childRef.current.focus();
this.childRef.current.getValue();
};
render() {
return (
<div>
<ChildInput ref={this.childRef} />
<button onClick={this.handleClick}>Focus Child</button>
</div>
);
}
}
class ChildInput extends React.Component {
inputRef = React.createRef();
focus = () => {
this.inputRef.current.focus();
};
getValue = () => {
return this.inputRef.current.value;
};
render() {
return <input ref={this.inputRef} />;
}
}
7. 自定义事件(Event Bus)
// eventBus.js
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
off(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
}
const eventBus = new EventBus();
// 组件 A - 发送事件
function ComponentA() {
const sendMessage = () => {
eventBus.emit('message', 'Hello from A');
};
return <button onClick={sendMessage}>Send</button>;
}
// 组件 B - 接收事件
function ComponentB() {
const [message, setMessage] = useState('');
useEffect(() => {
const handleMessage = (data) => {
setMessage(data);
};
eventBus.on('message', handleMessage);
return () => {
eventBus.off('message', handleMessage);
};
}, []);
return <div>Message: {message}</div>;
}
深入理解
不同通信方式的适用场景
| 通信方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props | 父子组件 | 简单直观 | 层级过深时繁琐 |
| 回调函数 | 子传父 | 符合单向数据流 | 需要额外定义 |
| Context | 跨层级通信 | 避免 prop drilling | 过度使用会导致重渲染 |
| Redux/Mobx | 全局状态 | 可预测、可调试 | 增加复杂度 |
| Ref | 获取实例/DOM | 直接操作 | 破坏封装性 |
| Event Bus | 任意组件 | 灵活 | 难以追踪、易内存泄漏 |
Context 的注意事项
// 问题:Provider 的 value 每次渲染都是新对象
function App() {
return (
<MyContext.Provider value={{ theme: 'dark' }}>
<Child />
</MyContext.Provider>
);
}
// 解决:使用 useMemo 缓存 value
function App() {
const value = useMemo(() => ({ theme: 'dark' }), []);
return (
<MyContext.Provider value={value}>
<Child />
</MyContext.Provider>
);
}
最佳实践
- 优先使用 Props:保持组件的纯粹和可预测
- 状态提升:兄弟组件通信时,将状态提升到共同父组件
- 适度使用 Context:适合主题、用户信息等全局数据,避免过度使用
- 谨慎使用全局状态:只有真正需要全局共享的数据才使用 Redux 等
- 避免过度使用 Ref:Ref 会破坏组件封装,优先使用 props 和回调
// 推荐的组件结构
function App() {
// 全局状态
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<ThemeProvider>
<Router>
<Routes />
</Router>
</ThemeProvider>
</UserContext.Provider>
);
}
面试要点
- 理解单向数据流:React 的设计理念,数据自顶向下流动
- 掌握多种通信方式:能够根据场景选择合适的方案
- 了解各方式的优缺点:避免过度设计或设计不足
- 实际项目经验:能够举例说明项目中使用的通信方案
常见追问:
- Context 和 Redux 有什么区别?
- 如何避免 Context 导致的过度渲染?
- Ref 和 props 有什么区别,什么时候使用 Ref?
- 什么是 prop drilling,如何解决?