背景:面试中面试官问到了组件通信,回答了props,useContext,组件库,面试官问我兄弟组件通信,提到这个没听说过的api,后续去了解一下useImperativeHandle
一、 角色分工:它们分别解决什么问题?
在正式写代码之前,我们先用一句话给这三个工具定性:
useRef(创建者) :React Hook在组件内部使用,用来放 DOM 节点或者不需要触发重新渲染的变量。forwardRef(传话筒) :React 默认不让父组件直接拿子组件的 Ref。forwardRef包裹组件,允许组件接收父组件传过来的 ref,并向下传递给子组件或 DOM 元素。useImperativeHandle(防盗门/过滤器) :如果子组件被forwardRef暴露了,父组件就能对它“为所欲为”。useImperativeHandle的作用就是限制父组件的权限,只允许父组件调用子组件明确暴露的几个方法。
二、 经典实战:兄弟组件的“跨界”通讯
在 React 中,兄弟组件由于没有直接的血缘关系,是无法直接对话的。
假设我们有一个场景:哥哥组件(ControlPanel 控制面板)里面有一个按钮,点击之后,需要让弟弟组件(Player 播放器)开始播放音乐。
如果用传统的“状态提升”,会导致父组件频繁重新渲染。今天我们用 Ref 全家桶 来实现一个高性能、精准控制的兄弟通信。
1. 弟弟组件:用 useImperativeHandle 打造安全接口
首先,弟弟组件(播放器)内部有自己的播放状态。它通过 forwardRef 接受父亲传来的管道,并用 useImperativeHandle 锁住自己,只暴露 startPlay 和 stopPlay 两个接口。
import React, { useState, forwardRef, useImperativeHandle } from 'react';
// 1. 使用 forwardRef 让子组件可以接收 ref
const Player = forwardRef((props, ref) => {
const [isPlaying, setIsPlaying] = useState(false);
// 2. 使用 useImperativeHandle 限制暴露的内容
useImperativeHandle(ref, () => {
return {
startPlay() {
setIsPlaying(true);
console.log("播放器:收到指令,开始播放...🎵");
},
stopPlay() {
setIsPlaying(false);
console.log("播放器:收到指令,暂停播放...⏸");
}
};
});
return (
<div style={{ border: '2px solid #007acc', padding: '15px', borderRadius: '8px' }}>
<h3>子组件:弟弟(Player 播放器)</h3>
<p>播放状态:{isPlaying ? '🎵 正在播放 Music...' : '⏸ 已暂停'}</p>
</div>
);
});
export default Player;
2. 哥哥组件:单纯的触发者
哥哥组件非常纯粹,它不需要知道弟弟是怎么播放音乐的,它只需要从父组件那里拿到一个“触发按钮的回调”即可。
import React from 'react';
function ControlPanel({ onPlayClick }) {
return (
<div style={{ border: '2px solid #228b22', padding: '15px', borderRadius: '8px', marginBottom: '15px' }}>
<h3>子组件:哥哥(ControlPanel 控制面板)</h3>
<button onClick={onPlayClick} style={{ padding: '8px 16px', cursor: 'pointer' }}>
命令弟弟:播放音乐
</button>
</div>
);
}
export default ControlPanel;
3. 父组件:牵线搭桥的“调度中心”
父组件就像是一个接线员。它用 useRef 生成一个遥控器(playerRef),把遥控器插在弟弟身上,然后把“按遥控器”的动作(handleBrotherClick)交给哥哥。
JavaScript
import React, { useRef } from 'react';
import ControlPanel from './ControlPanel';
import Player from './Player';
function App() {
// 1. 创建一个 ref 容器,充当遥控器
const playerRef = useRef(null);
// 2. 哥哥点击时的响应函数
const handleBrotherClick = () => {
// 4. 精准调用弟弟暴露出来的核心方法
if (playerRef.current) {
playerRef.current.startPlay();
}
};
return (
<div style={{ padding: '20px', maxWidth: '500px', margin: '50px auto', boxShadow: '0 0 10px #ccc' }}>
<h2>父组件(调度中心)</h2>
<hr />
{/* 3. 把触发权限给哥哥 */}
<ControlPanel onPlayClick={handleBrotherClick} />
{/* 3. 把遥控器绑定给弟弟 */}
<Player ref={playerRef} />
</div>
);
}
export default App;
三、 深度思考:为什么不直接用 Props(状态提升)?
看完这个例子,你可能会想:“我直接在父组件定义一个 isPlaying 的状态,然后用 props 传给弟弟不就行了吗?”
确实可以,那是标准的 React 单向数据流做法。但使用 useImperativeHandle 有两个无法替代的优势:
- 极致的渲染性能:如果使用 Props 改变状态,哥哥点击 -> 父组件 State 改变 -> 父组件及旗下所有子组件全部重新渲染。而使用 Ref 方式,哥哥点击后,直接通过引用唤醒弟弟,父组件完全不触发重新渲染。
- 完美的组件封装(高内聚) :播放器的“播放”和“暂停”涉及很多内部逻辑(比如音频上下文初始化、计时器重置等)。如果全部提升到父组件,父组件会变得极其臃肿。通过
useImperativeHandle,逻辑依然保留在子组件内部,父组件只需要负责“下达命令” 。