组件设计模式与通信

一、组件通信方式

1. 父 → 子:Props

jsx 复制代码
function Parent() {
  return <Child name="小明" age={18} />;
}

function Child({ name, age }) {
  return <p>{name},{age}岁</p>;
}

2. 子 → 父:回调函数

jsx 复制代码
function Parent() {
  const handleMsg = (msg) => console.log('收到:', msg);
  return <Child onSend={handleMsg} />;
}

function Child({ onSend }) {
  return <button onClick={() => onSend('你好')}>发送</button>;
}

3. 兄弟组件:状态提升

jsx 复制代码
// 把共享状态提升到共同的父组件
function Parent() {
  const [value, setValue] = useState('');
  return (
    <>
      <InputBox onChange={setValue} />
      <Display text={value} />
    </>
  );
}

4. 跨层级:Context

jsx 复制代码
const ThemeCtx = createContext('light');

function App() {
  return (
    <ThemeCtx.Provider value="dark">
      <Page />  {/* 中间隔了很多层 */}
    </ThemeCtx.Provider>
  );
}

function DeepChild() {
  const theme = useContext(ThemeCtx);  // 直接拿到,不用层层传递
  return <div className={theme}>内容</div>;
}

5. 全局状态:Redux / Zustand

适合大型应用,多个不相关组件需要共享状态时使用(详见 06.md)。

通信方式速查

场景 推荐方式
父 → 子 Props
子 → 父 回调函数
兄弟 状态提升
跨多层 Context
全局复杂状态 Redux / Zustand

二、高阶组件(HOC)

一句话:接收一个组件,返回一个增强后的新组件。本质是函数,不是组件。

jsx 复制代码
// HOC:给任意组件添加"加载中"功能
function withLoading(WrappedComponent) {
  return function EnhancedComponent({ isLoading, ...props }) {
    if (isLoading) return <div>加载中...</div>;
    return <WrappedComponent {...props} />;
  };
}

// 使用
const UserListWithLoading = withLoading(UserList);
<UserListWithLoading isLoading={true} users={[]} />

常见 HOC

  • React.memo() --- 性能优化
  • connect() --- Redux 连接(旧写法)
  • withRouter() --- React Router(旧写法)

HOC 的问题

  • 嵌套多了形成"包装地狱"
  • props 来源不清晰
  • 现在基本被 Hooks 替代了

三、Render Props

一句话:通过 props 传一个渲染函数,让组件把数据"交出来",由外部决定怎么渲染。

jsx 复制代码
// 鼠标位置追踪组件
function MouseTracker({ render }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });

  const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });

  return <div onMouseMove={handleMove}>{render(pos)}</div>;
}

// 使用:同一个逻辑,不同的渲染方式
<MouseTracker render={({ x, y }) => <p>鼠标在 ({x}, {y})</p>} />
<MouseTracker render={({ x, y }) => <div style={{ left: x, top: y }} className="cursor" />} />

现在也基本被自定义 Hook 替代了(useMousePosition)。

四、组合 vs 继承

React 推荐组合,不推荐继承。

jsx 复制代码
// ✅ 组合:通过 children 或 props 组合
function Card({ title, children }) {
  return (
    <div className="card">
      <h3>{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}

<Card title="用户信息">
  <p>姓名:小明</p>
  <p>年龄:18</p>
</Card>

// ✅ 插槽模式:多个内容区域
function Layout({ header, sidebar, content }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <aside>{sidebar}</aside>
      <main>{content}</main>
    </div>
  );
}

<Layout
  header={<NavBar />}
  sidebar={<Menu />}
  content={<Page />}
/>

五、错误边界(Error Boundary)

一句话:捕获子组件树的 JS 错误,显示降级 UI,防止整个页面白屏。

jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };  // 触发降级 UI
  }

  componentDidCatch(error, info) {
    console.error('组件错误:', error, info);  // 上报错误
  }

  render() {
    if (this.state.hasError) {
      return <h2>出错了,请刷新页面</h2>;
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary>
  <App />  {/* App 内部报错不会白屏,而是显示降级 UI */}
</ErrorBoundary>

注意:错误边界捕获不了这些错误:

  • 事件处理函数中的错误(用 try/catch)
  • 异步代码(setTimeout、Promise)
  • 服务端渲染
  • 错误边界自身的错误

目前只能用类组件写错误边界,函数组件暂不支持。可以用 react-error-boundary 库简化。

六、Portals

一句话:把组件渲染到 DOM 树的其他位置,而不是父组件内部。

jsx 复制代码
import { createPortal } from 'react-dom';

function Modal({ children, onClose }) {
  // 虽然 Modal 在组件树中是 App 的子组件
  // 但实际 DOM 渲染到了 document.body 下
  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body
  );
}

// 使用
function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <button onClick={() => setShow(true)}>打开弹窗</button>
      {show && <Modal onClose={() => setShow(false)}>弹窗内容</Modal>}
    </div>
  );
}

典型场景:弹窗(Modal)、提示框(Tooltip)、全局通知(Toast)。

Portal 虽然 DOM 位置变了,但 React 事件冒泡仍然按组件树走,不按 DOM 树走。

七、高频面试题

Q1:React 组件通信有哪些方式?

父→子用 Props,子→父用回调函数,兄弟用状态提升,跨层级用 Context,全局状态用 Redux/Zustand。

Q2:HOC 和自定义 Hook 怎么选?

优先用自定义 Hook。HOC 是类组件时代的方案,存在嵌套地狱和 props 来源不清的问题。Hook 更直观、更灵活、更容易组合。

Q3:为什么 React 推荐组合不推荐继承?

  • 组合更灵活:通过 props 和 children 可以任意组合
  • 继承耦合太强:子类依赖父类实现,改父类可能影响所有子类
  • React 官方明确说:在 Facebook 数千个组件中,没有找到需要继承的场景

Q4:错误边界能捕获所有错误吗?

不能。只能捕获渲染过程、生命周期、构造函数中的错误。事件处理、异步代码、服务端渲染中的错误捕获不了,需要用 try/catch 或 window.onerror。

Q5:Portal 的事件冒泡怎么走?

组件树冒泡,不按 DOM 树。即使 Modal 渲染到了 body 下,点击 Modal 内部的事件仍然会冒泡到 React 组件树中的父组件。

复制代码
相关推荐
Jinuss几秒前
源码分析之React中的useImperativeHandle
开发语言·前端·javascript
ZC跨境爬虫13 分钟前
CSS核心知识点与定位实战全解析(结合Playwright爬虫案例)
前端·css·爬虫
Jinuss14 分钟前
源码分析之React中的forwardRef解读
前端·javascript·react.js
mengsi5517 分钟前
Antigravity IDE 在浏览器上 verify 成功但本地 IDE 没反应 “开启Tun依然无济于事” —— 解决方案
前端·ide·chrome·antigravity
南风知我意95729 分钟前
JavaScript 惰性函数深度解析:从原理到实践的极致性能优化
开发语言·javascript·性能优化
Можно31 分钟前
pages.json 和 manifest.json 有什么作用?uni-app 核心配置文件详解
前端·小程序·uni-app
hzhsec34 分钟前
钓鱼邮件分析与排查
服务器·前端·安全·web安全·钓鱼邮件
sg_knight36 分钟前
设计模式实战:观察者模式(Observer)
python·观察者模式·设计模式
爱看老照片1 小时前
uniapp传递数值(数字)时需要使用v-bind的形式(加上冒号)
javascript·vue.js·uni-app