返回首页

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>
  );
}

最佳实践

  1. 优先使用 Props:保持组件的纯粹和可预测
  2. 状态提升:兄弟组件通信时,将状态提升到共同父组件
  3. 适度使用 Context:适合主题、用户信息等全局数据,避免过度使用
  4. 谨慎使用全局状态:只有真正需要全局共享的数据才使用 Redux 等
  5. 避免过度使用 Ref:Ref 会破坏组件封装,优先使用 props 和回调
// 推荐的组件结构
function App() {
  // 全局状态
  const [user, setUser] = useState(null);

  return (
    <UserContext.Provider value={user}>
      <ThemeProvider>
        <Router>
          <Routes />
        </Router>
      </ThemeProvider>
    </UserContext.Provider>
  );
}

面试要点

  1. 理解单向数据流:React 的设计理念,数据自顶向下流动
  2. 掌握多种通信方式:能够根据场景选择合适的方案
  3. 了解各方式的优缺点:避免过度设计或设计不足
  4. 实际项目经验:能够举例说明项目中使用的通信方案

常见追问:

  • Context 和 Redux 有什么区别?
  • 如何避免 Context 导致的过度渲染?
  • Ref 和 props 有什么区别,什么时候使用 Ref?
  • 什么是 prop drilling,如何解决?