组件通信
React 的组件通信从近到远可以分为:父子通信(Props/回调)、跨层级通信(Context / 发布订阅模式)、以及全局状态管理(Redux / Zustand / MobX 等)。
1. Props / Callback (常规父子通信)
最常规的通信方式,遵循 React 的单向数据流哲学。
- 父传子:父组件通过属性向下传递数据。
- 子传父:父组件将一个回调函数(Callback)通过 Props 传给子组件,子组件在需要时调用该函数,将数据作为参数回传给父组件。
- 面试坑点:如果是极其深层的嵌套(如 A -> B -> C -> D),一直通过 Props 往下传(Props Drilling)会导致中间组件被迫接收无关属性,引发无谓的重新渲染,且代码极难维护。
2. React Context (跨层级通信)
通过 createContext 创建一个上下文,专门用于解决 Props Drilling(属性钻取)的问题。
常规用法代码示例
创建 Context 并导出:
// ThemeContext.js
import { createContext } from "react";
export const ThemeContext = createContext("light"); // 默认值顶层组件提供数据 (Provider):
// App.js
import { ThemeContext } from "./ThemeContext";
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}底层组件消费数据 (useContext):
// Button.js (Toolbar的深层子组件)
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
function Button() {
const theme = useContext(ThemeContext);
return <button className={`btn-${theme}`}>按钮</button>; // 拿到 "dark"
}
底层原理
- Provider 传递:
Provider在 React render 过程中,当其 value 发生变化时,会向下遍历子代FiberNode,找到消费该 context 的FiberNode并提升其更新优先级。 - 栈 (Stack) 结构管理:当存在多个相同的 Provider 嵌套时,React 内部使用栈的形式维护。在
beginWork(递)时 push 进栈,在completeWork(归)时 pop 出栈,从而确保子组件永远取得的是离它最近的那个Provider的值。
🚨 面试高频灾难题:Context 穿透与性能陷阱
面试官问:使用 Context 有什么严重的性能隐患?你平时是怎么优化的?
性能陷阱(Context 穿透):Context 变动是无视
React.memo或者shouldComponentUpdate的拦截的。也就是说,只要 Provider 传入的value对象发生了一丁点变化(甚至只是引用地址变了),所有订阅了这个 Context(调用了useContext)的后代组件都会被强制重新渲染 (forceUpdate)!即使某个组件只用到了里面根本没变的一个小字段。三大终极优化方案(阻断 Context 穿透渲染):
- 切分上下文 (Split Context):不要把整个应用的状态塞进一个巨大的
AppStoreContext中。应该按业务拆分,比如分成ThemeContext(低频变化)和AuthContext。- 读写分离 (分离 State 和 Dispatch):将数据(State)和修改数据的方法(Dispatch)分到两个不同的 Context 中。这样当 State 变化时,那些只订阅了 Dispatch(只负责触发动作的按钮组件)就不会跟着无辜重渲染。
- 必加
useMemo+ 组件拆分结合React.memo:
- 首先,在 Provider 的 value 传入处,必须使用
useMemo缓存传入的对象。否则父组件一刷新,就会生成全新的字面量对象,导致所有子组件雪崩式重渲染。- 其次,如果一个大组件中只有一小块需要用到 Context,不要在顶层直接
useContext,而是把用到 Context 的那块代码单独抽离成一个很小的子组件,并在外层套上React.memo。这样 Context 更新只会打穿并渲染那个极小的子组件,大组件本身被memo保护。- 终极方案:放弃 Context,使用第三方状态库:如果你的数据更新极其频繁(比如画布拖拽坐标),不要用 Context!直接上 Zustand 或 MobX。它们底层基于发布订阅模式,可以做到精准的字段级更新(细粒度订阅),彻底解决渲染穿透问题。
❌ 错误示范(雪崩重渲染):
function App() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// 当 other 发生变化触发 App 重渲染时,这里的 { count, setCount } 是一个全新的对象地址
// 导致所有消费了 Context 的子组件全部被迫强制重新渲染!
return (
<MyContext.Provider value={{ count, setCount }}>
<Child />
</MyContext.Provider>
);
}✅ 正确示范(使用 useMemo 阻断渲染):
function App() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// 只有当 count 变化时,才会生成新的对象地址
// other 变化导致的 App 重渲染,不会波及到 Context 的消费者
const contextValue = useMemo(() => ({ count, setCount }), [count]);
return (
<MyContext.Provider value={contextValue}>
<Child />
</MyContext.Provider>
);
}
3. 全局状态管理 (Redux vs Zustand vs MobX)
在高级前端面试中,不仅要懂 API,更要懂技术选型背后的 Trade-off(权衡)。
3.1 Redux (传统老大哥)
- 核心理念:单一数据源、状态不可变(Immutable)、纯函数 Reducer 修改状态。
- 优点:时间旅行(Time Travel)调试体验极佳;状态流转极其可控,适合极其复杂的大型金融/中后台系统。
- 缺点:令人发指的模板代码(Boilerplate)。写一个功能要建 Action、Type、Reducer,开发心智负担极重。
- 底层原理:结合
react-redux,底层利用了 React 18 的新 HookuseSyncExternalStore来订阅外部状态,实现了细粒度更新,完美避开了 Context 的全量渲染陷阱。
3.2 MobX (响应式代表)
- 核心理念:基于响应式(Reactive)的面向对象状态管理,依赖收集(类似 Vue 的 Proxy / Object.defineProperty)。
- 优点:几乎没有模板代码,直接像修改普通 JS 对象一样
store.count++即可更新视图。由于底层是响应式的,谁用到了谁才更新,性能极佳。 - 缺点:状态修改过于自由,容易导致数据流向难以追踪;脱离了 React 的 Immutable 哲学。
3.3 Zustand (现代 React 社区新宠)
- 核心理念:极简的 Hooks 风格 API,即插即用。
- 优点:
- 极其轻量:源码极简,打包体积不到 2KB。
- 无需 Provider 包裹:打破了层级限制,不用在根节点包一层 Provider。
- 完美的细粒度订阅:通过 selector
const bears = useStore(state => state.bears)精准订阅所需片段。只有bears变了当前组件才渲染。 - 完全免疫 Context 陷阱:底层同样基于发布订阅和外部存储,不会引发无辜组件重渲染。
状态管理选型方案与权衡 (Zustand vs Redux vs MobX)
“在目前的新项目中,除非是百人协作的大型应用,否则我首选 Zustand。”
- 什么时候适合用 MobX?(性能极限与面向对象) 如果你的项目是重交互的图形编辑器(如在线 Excel、白板、Canvas 画图),里面充斥着几万个对象,且这些对象之间有复杂的关联计算。这种场景下:
- 性能极限:MobX 靠数据劫持做到了“魔法级”的细粒度更新,修改一个单元格的值绝不会导致整个表格重绘。
- 面向对象:它可以完美配合 OOP(面向对象编程),把逻辑封装在 Class 里。
- 缺点:违背 React 的 Immutable 哲学,
user.name = 'xxx'这种自由的修改会导致常规业务的数据流追踪变得极其困难。所以它只适合这种“偏客户端/游戏引擎”的重型 Web 软件。- 为什么日常业务首选 Zustand?(平衡的艺术) 绝大多数普通的后台/C端项目根本碰不到 MobX 的性能瓶颈。Zustand 坚守了 Immutable 的规矩,却把原本像 Redux 那样繁琐的模板代码砍到了极致。它的 Hooks API 设计最符合现代 React 直觉,性能足够好,且不破坏数据流规矩。
- 什么时候必须请出老大哥 Redux?(自由 vs 约束) Zustand 太自由,且异步处理偏弱。如果在金融或复杂中后台场景,Redux 具有不可替代的价值:
- 极致的调试审计:必须派发 Action 的死板规定,换来的是完美的时间旅行 (Time Travel) 和状态审计日志。
- 异步流控制:面对“竞态、轮询、重试”等变态异步流,
redux-saga的生态降维打击其他库。- 强架构约束:繁琐的模板代码本质上是给百人团队定下的“防呆规矩”,兜住了大型项目代码质量的下限。
4. Event Bus (发布订阅模式)
对于一些没有任何层级关系(如兄弟节点,甚至跨路由)的组件,偶尔需要发送一次性指令,可以使用 Event Bus(如 mitt 或自己手写一个简单的发布订阅类)。
- 优点:极度解耦。
- 缺点:滥用会导致代码变成“意大利面条”,很难追踪事件是从哪里发出来的;且必须记得在
useEffect的 cleanup 中调用off解绑,否则会导致内存泄漏。