React 框架指南

28 阅读18分钟

一、React 框架核心

1. 🌟JSX

JSX 是 JavaScript XML 的缩写,它允许我们在 JavaScript 里直接写类似 HTML 的标签。

const element = <h1 className="title">Hello, React!</h1>;

看着像模板语言,但它其实是 JavaScript 的语法扩展,最终会被编译成普通的 JS 函数调用。

  1. 为啥要用 JSX 🤔?
  • 声明式 UI:一眼就能看出页面长啥样,代码即 UI。
  • 逻辑与结构融合:可以在标签里用 {} 嵌入 JS 表达式,动态渲染数据。
  • 避免手写繁琐的创建元素代码
  1. JSX 语法规则
  • 必须只有一个根元素
    可以用 <></> (Fragment)包裹多个同级元素:

    return (
      <>
        <p>段落1</p>
        <p>段落2</p>
      </>
    );
    
  • JS 表达式用 {} 包裹
    可以放变量、函数调用、三元表达式等:

    const name = 'Alice';
    const isLoggedIn = true;
    return <div>{isLoggedIn ? `Hello, ${name}` : '请登录'}</div>;
    
  • 属性名采用驼峰式
    HTML 属性在 JSX 中要转换:

    • class ➡️ className
    • for ➡️ htmlFor
    • tabindex ➡️ tabIndex
    • 行内样式要传对象:style={{ color: 'red', fontSize: '16px' }}
  • 支持嵌套
    可以像 HTML 一样自由嵌套子元素:

    return (
      <div>
        <header>头部</header>
        <main>内容</main>
      </div>
    );
    
  1. JSX 的底层秘密:编译过程

浏览器不认识 JSX,所以需要工具(比如 Babel)把它编译成 React.createElement 调用。

const element = <h1 className="greeting">Hello, world!</h1>;

编译后:

const element = React.createElement('h1', { className: 'greeting' }, 'Hello, world!');

所以说 JSX 只是 createElement 的语法糖,但用起来爽多了!

2. ⚙️React.createElement

React.createElement 是 React 用来创建 虚拟 DOM 元素 的核心方法。虽然平时我们用 JSX 很少直接碰它,但理解它能让你对 React 渲染机制更通透。

React.createElement(type, props, ...children)
  • type:元素类型,可以是标签名字符串(如 'div')、组件(函数或类)、Fragment。
  • props:属性对象,没有传 null{}
  • children:子节点,可以写多个,比如文本、其他元素、数组。

返回一个 React 元素,本质是一个普通 JS 对象,描述了你想要在屏幕上看到什么。

const element = React.createElement('h1', null, 'Hello');
// 得到的对象大概长这样:
// { type: 'h1', props: { children: 'Hello' }, key: null, ref: null, ... }

手写示例,感受一下没有 JSX 的世界

// 创建一个 div,里面包含一个 h1 和一个 p
const element = React.createElement(
  'div',
  { className: 'wrapper' },
  React.createElement('h1', null, '标题'),
  React.createElement('p', null, '这是一段描述')
);

是不是比 JSX 麻烦多了?所以 JSX 是真香~

JSX 是 createElement 的语法糖,所有 JSX 最后都会被 Babel 转成 createElement 调用。所以两者本质是一回事,只是写法不同。

3. 🚪React.createPortal

ReactDOM.createPortal 可以把子节点渲染到父组件 DOM 树之外的指定 DOM 节点上。

ReactDOM.createPortal(jsx, 真实的DOM容器)
  • jsx:要渲染的内容(可以是任意 React 元素)
  • 容器:一个已经存在的 DOM 元素,比如 document.body

有些 UI 组件需要脱离父容器的 CSS 限制,比如:

  • 模态框(Modal):希望浮在整个页面最上层,不被父组件的 overflow: hidden 裁剪。
  • 提示框(Tooltip):相对于某个元素定位,但不想受父组件影响。
  • 下拉菜单、悬浮卡片等。

如果用普通方式,这些组件嵌套在父组件里,很容易被父组件的样式或层级关系干扰。Portal 让它们渲染到 body 下,完美解决!

Portal 的特殊之处

  • DOM 位置变了,但逻辑还在 React 树里
    尽管渲染到了别处,但组件依然能接收到来自父组件的 props、context,事件也会按照 React 组件树冒泡(而不是 DOM 树)。

  • 生命周期和父组件绑定
    父组件卸载时,Portal 内的组件也会被卸载。

代码示例:一个简单的模态框

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

function Modal({ children, onClose }) {
  return ReactDOM.createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>,
    document.body // 直接丢到 body 下
  );
}

function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <button onClick={() => setShow(true)}>打开模态框</button>
      {show && (
        <Modal onClose={() => setShow(false)}>
          <h2>我是模态框</h2>
          <p>我在 body 里,但事件依然能触发 App 里的关闭函数!</p>
          <button onClick={() => setShow(false)}>关闭</button>
        </Modal>
      )}
    </div>
  );
}

注意:点击模态框内容不会关闭(因为阻止了冒泡),但点击遮罩层会关闭,这说明事件从 Portal 内部冒泡到了 App 组件。

注意事项

  • Portal 的目标容器必须先在 HTML 中存在(比如 <div id="modal-root"></div> 或直接用 document.body)。
  • 对 React 组件树而言,Portal 只是改变了渲染位置,并没有改变父子关系,所以 context 传递、事件冒泡都正常工作。

⚠️ 总结

  • JSX:写 UI 的优雅语法,底层是 createElement 的语法糖,必须掌握!
  • createElement:创建虚拟 DOM 的底层 API,了解它有助于深入理解 React 渲染。
  • createPortal:把组件渲染到任意 DOM 节点的技巧,专治弹窗、提示框等“越界”需求。

4. 💡$$typeof

JSX 和 createElement,生成了一个 React 元素对象,它大概长这样:

{
  type: 'h1',
  props: { children: 'Hello' },
  key: null,
  ref: null,
  // 咦,这个 $$typeof 是啥?
  $$typeof: Symbol.for('react.element')
}

没错,每个 React 元素身上还藏着一个 $$typeof 属性!它就像元素的“身份证”,用来告诉 React:“我是一个真正的 React 元素,不是路边随便捡来的对象!”

🔍 为啥需要这个身份证

一切都要从 安全 说起。

在早期 React 版本(0.14 之前),React 元素就是一个普通的对象,比如:

const element = { type: 'div', props: { children: '文本' } };

React 看到这种对象,就直接拿去渲染了。

但问题来了:如果服务端返回了一个恶意构造的 JSON 数据,里面包含这样的对象:

{
  "type": "div",
  "props": {
    "dangerouslySetInnerHTML": {
      "__html": "<img src=x onerror='alert(\"XSS\")' />"
    }
  }
}

如果客户端直接把这个 JSON 当作 React 元素使用,就会执行恶意脚本,造成 XSS 攻击

🛡️解决方案:带上独一无二的标识

React 团队想了个办法:在创建元素时,给每个元素打上一个 唯一的 Symbol 标记$$typeof)。

Symbol.for('react.element')

这个 Symbol 是全局唯一的,只有通过 React.createElement 或 JSX 创建的元素才会带上它。

当 React 渲染一个对象时,会先检查它的 $$typeof 是不是正确的 Symbol。如果不是,就直接拒绝渲染。

这样一来,黑客从服务端返回的 JSON 里伪造的“元素”因为没有 $$typeof(JSON 无法包含 Symbol 值),就会被 React 安全地忽略掉,XSS 攻击自然就被防住了~

🧪 其他类型的 $$typeof

除了 react.element,React 内部还有其他类型的 $$typeof 值,用来区分不同的 React 节点类型:

  • Symbol.for('react.portal'):Portal 节点
  • Symbol.for('react.fragment'):Fragment 节点
  • Symbol.for('react.strict_mode'):StrictMode 节点
  • Symbol.for('react.profiler'):Profiler 节点
  • Symbol.for('react.provider') / Symbol.for('react.context'):Context 相关

$$typeof 是 React 元素的一个 内部安全标识,用来防止恶意伪造的元素被渲染,是 React 安全防线的重要一环。

🔍React.isValidElement:辨别“真假美猴王”的火眼金睛

React.isValidElement 是一个静态方法,用来判断一个对象是否是合法的 React 元素

它的原理其实很简单:检查对象上是否有 $$typeof 属性,并且值等于 Symbol.for('react.element')

const element = <h1>Hello</h1>;
console.log(React.isValidElement(element)); // true

const notElement = { type: 'div', props: {} };
console.log(React.isValidElement(notElement)); // false(没有正确的 $$typeof)

const string = 'hello';
console.log(React.isValidElement(string)); // false

const number = 123;
console.log(React.isValidElement(number)); // false

在处理用户输入或外部数据时,先用 isValidElement 检查,避免把非法对象传给 React 渲染引擎

5. 🧸React Children

children 是 React 组件的一个 特殊 prop,它代表组件的“子节点”。简单说,就是写在组件标签内部的内容。

function Card({ children }) {
  return <div className="card">{children}</div>;
}

// 使用 Card 组件
<Card>
  <h2>标题</h2>
  <p>这是一段内容</p>
</Card>

这里的 <h2><p> 就是 Card 组件的 children,会被渲染到卡片内部。

1. children 可以是啥?

children 的类型非常灵活,几乎可以是任何东西:

  • 字符串/数字<Card>文本内容</Card>
  • JSX 元素<Card><div>内容</div></Card>
  • 组件<Card><Header /></Card>
  • 数组<Card>{[<li>1</li>, <li>2</li>]}</Card>
  • 函数:Render Props
  • 布尔值/null/undefined:这些会被忽略,不会渲染

2. 🎨children 的常见用法

  1. 基础用法:充当占位符

最常用的场景就是做布局组件,比如卡片、模态框、弹窗等,让外部决定具体内容。

function Modal({ children, isOpen }) {
  if (!isOpen) return null;
  return (
    <div className="modal-overlay">
      <div className="modal-content">{children}</div>
    </div>
  );
}
  1. 多个 children 的协作

有时候我们需要在组件里放多个“插槽”,比如头部、内容、底部。

function Layout({ header, children, footer }) {
  return (
    <div className="layout">
      <div className="header">{header}</div>
      <div className="main">{children}</div>
      <div className="footer">{footer}</div>
    </div>
  );
}

// 使用
<Layout 
  header={<h1>网站标题</h1>}
  footer={<div>版权信息</div>}
>
  <p>这是主要内容</p>
</Layout>
  1. 传递额外的 props 给 children

有时候我们需要修改或增强 children,这时候可以用 React.Children.map 配合 React.cloneElement

function RadioGroup({ children, name }) {
  return (
    <div>
      {React.Children.map(children, child => {
        // 给每个单选框自动添加 name 属性
        return React.cloneElement(child, { name });
      })}
    </div>
  );
}

// 使用
<RadioGroup name="gender">
  <input type="radio" value="male" /> 男
  <input type="radio" value="female" /> 女
</RadioGroup>

3. 🔧 React.Children工具方法

React 提供了一个 React.Children 工具集,专门用来处理 children 这个不透明的数据结构。

  1. React.Children.map

安全的遍历 children,不用担心它是单个元素还是数组。

React.Children.map(children, (child, index) => {
  // 对每个 child 进行处理
  return <li>{child}</li>;
})
  1. React.Children.forEach

跟 map 类似,但不返回新数组,适合做副作用操作。

React.Children.forEach(children, child => {
  console.log(child.type); // 打印每个子元素的类型
});
  1. React.Children.count

统计子元素的数量,包括文本节点。

function ItemsCount({ children }) {
  return <div>共有 {React.Children.count(children)} 个项目</div>;
}
  1. React.Children.only

确保只有一个子元素,否则抛错。

function MustHaveOneChild({ children }) {
  const child = React.Children.only(children);
  return <div className="wrapper">{child}</div>;
}
  1. React.Children.toArray

将 children 转换成扁平数组,方便操作。

function ReverseChildren({ children }) {
  const childrenArray = React.Children.toArray(children);
  return <div>{childrenArray.reverse()}</div>;
}

4. 🚀 children 的高级玩法

  1. 函数作为 children(Render Props)

把 children 定义成一个函数,让组件内部调用它并传入数据。

function DataFetcher({ url, children }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch(url).then(res => res.json()).then(setData);
  }, [url]);
  
  // 把数据通过函数传出去
  return children(data);
}

// 使用
<DataFetcher url="/api/user">
  {data => (
    <div>
      {data ? `你好, ${data.name}` : '加载中...'}
    </div>
  )}
</DataFetcher>
  1. 条件渲染 children

根据某些条件决定是否渲染 children。

function AuthGuard({ isLoggedIn, children }) {
  if (!isLoggedIn) {
    return <div>请先登录</div>;
  }
  return children;
}
  1. 过滤/筛选 children

只渲染符合特定条件的子元素。

function OnlyEven({ children }) {
  return (
    <div>
      {React.Children.map(children, (child, index) => {
        // 只保留偶数索引的子元素(0,2,4...)
        if (index % 2 === 0) {
          return child;
        }
        return null;
      })}
    </div>
  );
}
  1. children 的类型检查

结合 PropTypes 对 children 做类型约束。

import PropTypes from 'prop-types';

function Menu({ children }) {
  return <nav>{children}</nav>;
}

Menu.propTypes = {
  children: PropTypes.node, // 任何可渲染内容
  // 或者更严格的约束
  // children: PropTypes.element.isRequired, // 必须是 React 元素
  // children: PropTypes.arrayOf(PropTypes.element) // 必须是元素数组
};

5. ⚠️ 常见坑点与注意事项

  1. 不要直接修改 children

children 是只读的,直接修改会导致不可预测的问题。

// ❌ 错误
function BadComponent({ children }) {
  children.type = 'div'; // 不要这样!
  return children;
}

// ✅ 正确:用 cloneElement 创建新元素
function GoodComponent({ children }) {
  return React.cloneElement(children, { className: 'wrapper' });
}
  1. children 可能是单个或多个

处理 children 时要用 React.Children 方法,不要假设它是数组。

// ❌ 错误
function Bad({ children }) {
  return <div>{children.map(child => child)}</div>; // 如果 children 不是数组,会报错!
}

// ✅ 正确
function Good({ children }) {
  return <div>{React.Children.map(children, child => child)}</div>;
}
  1. falsy 值会被渲染?

注意:0 会被渲染,false/null/undefined/true 不会。

// 页面会显示 0
<div>
  {0 && <p>不会显示</p>}
</div>

// 正确写法
<div>
  {count > 0 && <p>显示内容</p>}
</div>
  1. Fragment 的 children 特殊处理

Fragment 不会在 DOM 中创建额外节点,但它的 children 会被正常处理。

function List({ children }) {
  return (
    <ul>
      {React.Children.map(children, child => (
        <li>{child}</li>
      ))}
    </ul>
  );
}

// 使用 Fragment 传入多个元素
<List>
  <>苹果</>  {/* Fragment 包裹 */}
  <>香蕉</>
  <>橙子</>
</List>

💡 一个灵活的 Tab 组件

function Tabs({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  
  // 提取所有 TabPane
  const panes = React.Children.toArray(children).filter(
    child => child.type === TabPane
  );
  
  return (
    <div className="tabs">
      <div className="tab-header">
        {panes.map((pane, index) => (
          <button
            key={index}
            className={index === activeIndex ? 'active' : ''}
            onClick={() => setActiveIndex(index)}
          >
            {pane.props.title}
          </button>
        ))}
      </div>
      <div className="tab-content">
        {panes[activeIndex]}
      </div>
    </div>
  );
}

function TabPane({ title, children }) {
  return <div className="tab-pane">{children}</div>;
}

// 使用
<Tabs>
  <TabPane title="个人资料">
    <p>这里是个人资料内容</p>
  </TabPane>
  <TabPane title="账号设置">
    <p>这里是账号设置内容</p>
  </TabPane>
</Tabs>
  • children 是 React 组件组合的核心,让组件像 HTML 标签一样灵活嵌套
  • React.Children 工具集提供了安全遍历、操作 children 的方法
  • React.cloneElement 可以给 children 传递额外的 props
  • 函数作为 children 实现了数据逻辑与 UI 分离的高级模式
  • 注意坑点:不要直接修改 children,注意 falsy 值的处理

二、React 组件

React 组件就是返回 React 元素的 JavaScript 函数或类。它接收一些参数(props),返回描述 UI 的元素。

// 最简单的组件
function Welcome() {
  return <h1>Hello, React!</h1>;
}

1. 组件的核心价值

  • 复用性:一次编写,多处使用
  • 可组合:组件里套组件,像搭积木
  • 独立维护:每个组件有自己的逻辑和样式
  • 单向数据流:数据从父流向子,清晰可预测

2. 组件定义

1. 函数组件(Function Component)

最简单、最现代的方式,就是一个普通的 JavaScript 函数。

// 基础写法
function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

// 箭头函数写法(更简洁)
const Greeting = ({ name }) => <h1>Hello, {name}!</h1>;

// 使用
<Greeting name="Alice" />

特点

  • 就是个函数,接收 props 返回 JSX
  • 没有自己的 this
  • React 16.8 之前叫“无状态组件”,之后有了 Hooks,功能跟类组件一样强大
  • 现在官方推荐优先使用函数组件 + Hooks

2. 类组件(Class Component)

ES6 class 的写法,继承自 React.Component

import React, { Component } from 'react';

class Greeting extends Component {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

特点

  • 必须包含 render() 方法
  • 有自己的状态 state 和生命周期
  • 可以通过 this 访问 props、state、方法
  • 现在主要用于维护老项目,新项目推荐函数组件

3. 组件的核心要素

1. Props:组件的“入参”

Props 是父组件传给子组件的数据,是只读的,子组件不能修改。

// 父组件
function App() {
  return (
    <div>
      <UserCard 
        name="张三"
        age={25}
        isVIP={true}
        hobbies={['读书', '跑步']}
        onUpdate={() => console.log('更新')}
      />
    </div>
  );
}

// 子组件
function UserCard({ name, age, isVIP, hobbies, onUpdate }) {
  return (
    <div className={`card ${isVIP ? 'vip' : ''}`}>
      <h2>{name}</h2>
      <p>年龄:{age}</p>
      <p>爱好:{hobbies.join(', ')}</p>
      <button onClick={onUpdate}>更新</button>
    </div>
  );
}

Props 传参技巧

  • 默认值UserCard.defaultProps = { name: '匿名' }
  • 类型检查:用 PropTypes 库
  • children:特殊的 prop,代表子元素
  • 展开传递<Child {...props} />

2. State:组件的“记忆”

State 是组件内部管理的数据,可以随时间变化。

import { useState } from 'react';

function Counter() {
  // 声明状态变量 count,初始值 0
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点我
      </button>
    </div>
  );
}

State 原则

  • 不能直接修改:要用 setStatesetCount
  • 更新可能是异步的:React 会批量处理
  • 不可变性:每次更新都是新值,而不是修改原值

3. 生命周期:组件的“生老病死”

组件从创建到销毁会经历一系列阶段。

函数组件的 Hooks 模拟

import { useState, useEffect } from 'react';

function LifecycleDemo() {
  // 挂载阶段
  useEffect(() => {
    console.log('组件已挂载');
    
    // 卸载阶段(清理函数)
    return () => {
      console.log('组件即将卸载');
    };
  }, []); // 空依赖数组,只在挂载/卸载时执行
  
  // 更新阶段
  useEffect(() => {
    console.log('组件更新了');
  }); // 没有依赖数组,每次渲染都执行
  
  // 特定状态更新
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log('count 变了:', count);
  }, [count]); // 只在 count 变化时执行
  
  return <div>生命周期演示</div>;
}

4. ref:React 提供的“逃生舱”

Ref 是 React 提供的“逃生舱”,让我们能直接访问 DOM 节点或组件实例。

function App() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus(); // 直接操作 DOM
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

默认情况下,函数组件不接收 ref 参数,因为函数组件没有实例。

// ❌ 这样不行!
function MyInput() {
  return <input />;
}

function App() {
  const inputRef = useRef(null);
  // 报错:Function components cannot be given refs
  return <MyInput ref={inputRef} />;
}

forwardRef 是一个高阶组件,它让函数组件能够接收 ref 参数,并把 ref 转发给子组件。

// React 源码简化版
export function forwardRef(render) {
  // 返回一个特殊的组件类型
  return {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render
  };
}

// 当 React 渲染这个组件时
// <MyInput ref={ref} /> 会被处理成
// render(props, ref)
import { forwardRef } from 'react';

// ✅ 正确用法
const MyInput = forwardRef((props, ref) => {
  return <input ref={ref} {...props} />;
});

function App() {
  const inputRef = useRef(null);
  
  return (
    <div>
      <MyInput ref={inputRef} placeholder="请输入" />
      <button onClick={() => inputRef.current.focus()}>
        聚焦
      </button>
    </div>
  );
}

forwardRef 的原理:它返回一个新组件,这个组件可以接收 ref 参数,并把 ref 传递给内部的实际 DOM 节点或组件。

Ref 是“逃生舱”,应该在必须直接操作 DOM 或组件实例时使用。能用 state 解决的问题不要用 ref。

如果需要转发多个 ref,可以用对象形式或合并:

const ComplexComponent = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  const divRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    input: inputRef.current,
    div: divRef.current,
    focus: () => inputRef.current.focus()
  }));
  
  return (
    <div ref={divRef}>
      <input ref={inputRef} />
    </div>
  );
});

4. 组件的通信方式

1. 父子通信:Props 向下传递

父传子用 props,子传父用回调函数。

function Parent() {
  const [childData, setChildData] = useState('');
  
  const handleChildData = (data) => {
    setChildData(data);
  };
  
  return (
    <div>
      <h2>来自子组件:{childData}</h2>
      <Child onSend={handleChildData} />
    </div>
  );
}

function Child({ onSend }) {
  return (
    <button onClick={() => onSend('Hello 爸爸!')}>
      给爸爸发消息
    </button>
  );
}

2. 兄弟通信:状态提升

把共享状态提升到最近的共同父组件。

function Parent() {
  const [selectedItem, setSelectedItem] = useState(null);
  
  return (
    <div>
      <List onSelect={setSelectedItem} />
      <Detail item={selectedItem} />
    </div>
  );
}

3. 跨级通信:Context

避免 props 层层传递的“props drilling”。

import { createContext, useContext } from 'react';

// 1. 创建 Context
const ThemeContext = createContext('light');

function App() {
  return (
    // 2. 提供 Context 值
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return <ThemedButton />;
}

function ThemedButton() {
  // 3. 使用 Context
  const theme = useContext(ThemeContext);
  return <button className={theme}>主题按钮</button>;
}

4. useImperativeHandle

有时候我们不想直接把整个 DOM 节点暴露给父组件,只想暴露特定的方法,比如 focusscrollTo等。这时候就需要 useImperativeHandle

import { forwardRef, useImperativeHandle, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);
  
  // 自定义暴露给父组件的方法和属性
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    blur: () => {
      inputRef.current.blur();
    },
    setValue: (value) => {
      inputRef.current.value = value;
    },
    getValue: () => {
      return inputRef.current.value;
    }
  }));
  
  return <input ref={inputRef} {...props} />;
});

function App() {
  const inputRef = useRef(null);
  
  const handleClick = () => {
    inputRef.current.focus();
    inputRef.current.setValue('Hello!');
    console.log('当前值:', inputRef.current.getValue());
  };
  
  return (
    <div>
      <CustomInput ref={inputRef} placeholder="请输入" />
      <button onClick={handleClick}>操作输入框</button>
    </div>
  );
}
useImperativeHandle(ref, createHandle, [deps])
  • ref:从 forwardRef 接收的 ref
  • createHandle:函数,返回一个对象,包含要暴露的方法和属性
  • [deps] (可选):依赖数组,当依赖变化时重新创建暴露的对象

5. 组件的进阶模式

1. 容器组件 vs 展示组件

  • 容器组件:负责数据获取、状态管理(“聪明组件”)
  • 展示组件:负责 UI 渲染(“傻瓜组件”)
// 容器组件
function UserContainer() {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser().then(setUser);
  }, []);
  
  return <UserDisplay user={user} />;
}

// 展示组件
function UserDisplay({ user }) {
  if (!user) return <div>加载中...</div>;
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

2. 高阶组件(HOC)

高阶组件是一个函数,它接收一个组件作为参数,并返回一个新的增强后的组件。听起来有点抽象,但其实就是“组件工厂”或“组件包装器”。

// 最简单的 HOC
function withExtraProps(WrappedComponent) {
  return function EnhancedComponent(props) {
    // 给原组件添加一些额外的 props
    return <WrappedComponent extra="我是新增的" {...props} />;
  };
}

为什么需要 HOC?

  • 逻辑复用:多个组件有相同的逻辑,抽离出来复用
  • 横切关注点:比如日志记录、权限控制、数据获取等
  • 增强组件:给现有组件添加功能而不修改源码
  • 条件渲染:根据条件决定是否渲染组件

HOC 主要有两种实现方式:属性代理(Props Proxy)  和 反向继承(Inheritance Inversion)

📦 属性代理(Props Proxy)

属性代理是最常见、最简单的 HOC 实现方式。它通过包装原组件,在返回的新组件中渲染原组件,同时可以操作 props、state、渲染结果等。

function withPropsProxy(WrappedComponent) {
  return function PropsProxy(props) {
    // 在这里可以操作 props、添加逻辑等
    return <WrappedComponent {...props} />;
  };
}

① 操作 props

可以添加、修改、过滤传递给原组件的 props。

// 给组件添加默认 props
function withDefaultProps(WrappedComponent, defaultProps) {
  return function PropsProxy(props) {
    // 合并默认 props 和传入的 props
    const mergedProps = { ...defaultProps, ...props };
    return <WrappedComponent {...mergedProps} />;
  };
}

// 使用
const Button = ({ color, text }) => (
  <button style={{ color }}>{text}</button>
);

const DefaultButton = withDefaultProps(Button, { 
  color: 'blue', 
  text: '默认按钮' 
});

// 渲染:<button style="color: blue;">默认按钮</button>
<DefaultButton />
// 也可以覆盖:<button style="color: red;">提交</button>
<DefaultButton color="red" text="提交" />

② 状态管理

可以在 HOC 内部管理 state,并通过 props 传递给原组件。

// 给组件添加计数功能
function withCounter(WrappedComponent) {
  return function WithCounter(props) {
    const [count, setCount] = useState(0);
    
    const increment = () => setCount(c => c + 1);
    const decrement = () => setCount(c => c - 1);
    
    return (
      <WrappedComponent
        count={count}
        increment={increment}
        decrement={decrement}
        {...props}
      />
    );
  };
}

// 使用
function CounterDisplay({ count, increment, decrement }) {
  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

const EnhancedCounter = withCounter(CounterDisplay);

③ 条件渲染

根据某些条件决定是否渲染原组件,或者渲染其他内容。

// 权限控制 HOC
function withAuth(WrappedComponent) {
  return function WithAuth(props) {
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    
    useEffect(() => {
      // 检查登录状态
      checkAuth().then(setIsLoggedIn);
    }, []);
    
    if (!isLoggedIn) {
      return <div>请先登录</div>;
    }
    
    return <WrappedComponent {...props} />;
  };
}

// 加载状态 HOC
function withLoading(WrappedComponent) {
  return function WithLoading({ isLoading, ...props }) {
    if (isLoading) {
      return <div className="spinner">加载中...</div>;
    }
    return <WrappedComponent {...props} />;
  };
}

④ 访问 ref

通过 forwardRef 结合属性代理,可以转发 ref。

function withRef(WrappedComponent) {
  return forwardRef((props, ref) => {
    return <WrappedComponent {...props} forwardedRef={ref} />;
  });
}

// 使用
const FancyInput = withRef(({ forwardedRef, ...props }) => (
  <input ref={forwardedRef} {...props} />
));

属性代理的优点:

  • 简单直观:容易理解和实现
  • 组合方便:多个 HOC 可以轻松组合
  • 不会破坏原组件:只是包装,不修改原组件
  • 类型推导友好:TypeScript 支持较好

属性代理的缺点:

  • 无法直接修改原组件的生命周期:只能通过 props 间接影响
  • props 命名冲突:如果添加的 props 和原组件 props 重名,会覆盖
  • 静态方法丢失:原组件的静态方法不会自动传递
🔄 反向继承(Inheritance Inversion)

反向继承的 HOC 返回的组件继承自原组件,可以访问原组件的内部状态、生命周期方法等。之所以叫“反向继承”,是因为 HOC 返回的组件继承了原组件,而不是原组件继承 HOC。

function withInheritance(WrappedComponent) {
  return class InheritanceInversion extends WrappedComponent {
    render() {
      return super.render();
    }
  };
}

① 渲染劫持(最核心能力)

可以读取、修改、甚至替换原组件的渲染结果。

// 给所有内容添加边框
function withBorder(WrappedComponent) {
  return class WithBorder extends WrappedComponent {
    render() {
      // 获取原组件的渲染结果
      const elements = super.render();
      
      // 在外面包一层带边框的 div
      return (
        <div style={{ border: '2px solid red', padding: '10px' }}>
          {elements}
        </div>
      );
    }
  };
}

// 条件渲染劫持
function withConditionalRender(WrappedComponent) {
  return class WithCondition extends WrappedComponent {
    render() {
      // 根据 props 决定是否渲染
      if (this.props.shouldRender === false) {
        return <div>组件被隐藏</div>;
      }
      
      // 或者修改原组件的 props
      const originalRender = super.render();
      return React.cloneElement(originalRender, {
        className: 'enhanced-class'
      });
    }
  };
}

② 操作 state

可以读取、修改原组件的 state。

// 日志记录 HOC
function withStateLogger(WrappedComponent) {
  return class WithLogger extends WrappedComponent {
    componentDidUpdate() {
      // 记录 state 变化
      console.log('State updated:', this.state);
    }
    
    render() {
      return super.render();
    }
  };
}

// 修改 state
function withResetState(WrappedComponent) {
  return class WithReset extends WrappedComponent {
    resetState = () => {
      this.setState(this.constructor.defaultState || {});
    };
    
    render() {
      // 添加 reset 方法
      const elements = super.render();
      return React.cloneElement(elements, {
        resetState: this.resetState
      });
    }
  };
}

③ 拦截生命周期

可以在原组件的生命周期前后添加逻辑。

// 性能监控 HOC
function withPerformanceTracking(WrappedComponent) {
  return class WithTracking extends WrappedComponent {
    componentDidMount() {
      console.time(`${WrappedComponent.name} 挂载时间`);
      if (super.componentDidMount) {
        super.componentDidMount();
      }
      console.timeEnd(`${WrappedComponent.name} 挂载时间`);
    }
    
    shouldComponentUpdate(nextProps, nextState) {
      console.log('是否应该更新?', {
        currentProps: this.props,
        nextProps,
        currentState: this.state,
        nextState
      });
      
      // 可以调用原组件的 shouldComponentUpdate
      if (super.shouldComponentUpdate) {
        return super.shouldComponentUpdate(nextProps, nextState);
      }
      return true;
    }
    
    render() {
      return super.render();
    }
  };
}

④ 错误边界

可以利用反向继承实现错误边界。

function withErrorBoundary(WrappedComponent, fallbackUI) {
  return class WithErrorBoundary extends WrappedComponent {
    constructor(props) {
      super(props);
      this.state = { hasError: false, error: null };
    }
    
    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }
    
    componentDidCatch(error, errorInfo) {
      console.error('捕获到错误:', error, errorInfo);
    }
    
    render() {
      if (this.state.hasError) {
        return fallbackUI || <div>出错了!</div>;
      }
      return super.render();
    }
  };
}

反向继承的优点:

  • 最强大:可以访问原组件的一切(state、生命周期、方法)
  • 渲染劫持:可以完全控制渲染输出
  • 深度集成:适合需要侵入组件内部的场景

反向继承的缺点:

  • 复杂且危险:容易破坏原组件的逻辑
  • 耦合度高:HOC 和原组件强依赖
  • 不推荐滥用:React 官方更推荐组合而非继承
  • TypeScript 支持较差:类型推导复杂
维度属性代理反向继承
实现方式包装组件,返回新组件继承原组件,返回新类
访问权限只能通过 props可访问 state、生命周期、方法
渲染控制控制包装层可完全劫持渲染结果
复杂度简单复杂
风险高(可能破坏原组件)
使用场景大多数情况需要深度侵入时
组合性容易组合组合困难
性能可能有额外开销

Hooks 取代了很多 HOC 的场景,如下所示:

// 以前用 HOC
function withWindowWidth(WrappedComponent) {
  return function WithWindowWidth(props) {
    const [width, setWidth] = useState(window.innerWidth);
    // ... 逻辑
    return <WrappedComponent width={width} {...props} />;
  };
}

// 现在用自定义 Hook
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  return width;
}

// 直接在组件中使用
function MyComponent() {
  const width = useWindowWidth();
  return <div>窗口宽度:{width}</div>;
}

Render Props 也可以替代:

// HOC 方式
function withMouse(WrappedComponent) {
  return function WithMouse(props) {
    const [position, setPosition] = useState({ x: 0, y: 0 });
    // ... 逻辑
    return <WrappedComponent mouse={position} {...props} />;
  };
}

// Render Props 方式
function Mouse({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  // ... 逻辑
  return children(position);
}

// 使用
<Mouse>
  {position => <div>x: {position.x}, y: {position.y}</div>}
</Mouse>
  • HOC 是 React 中经典的逻辑复用模式,虽然现代 React 有了 Hooks,但在很多场景依然很有价值
  • 两种实现方式
    • 属性代理:简单、安全、常用,通过包装组件实现
    • 反向继承:强大、危险、少用,通过继承组件实现
  • 最佳实践
    • 命名规范:withXxx
    • 传递静态方法
    • 处理 ref
    • 组合使用 compose
    • 不要修改原组件
  • 适用场景
    • 横切关注点(日志、权限、性能监控)
    • 代码复用(数据获取、状态管理)
    • 条件渲染(加载状态、错误边界)

3. Render Props

通过函数 prop 共享代码逻辑。

function MouseTracker({ children }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const handleMouseMove = (e) => {
    setPosition({ x: e.clientX, y: e.clientY });
  };
  
  return (
    <div onMouseMove={handleMouseMove}>
      {children(position)}
    </div>
  );
}

// 使用
<MouseTracker>
  {({ x, y }) => (
    <p>鼠标位置:{x}, {y}</p>
  )}
</MouseTracker>

4. 复合组件

多个组件协同工作,像 <select><option>

function Tab({ children }) {
  const [activeIndex, setActiveIndex] = useState(0);
  
  // 提取 TabItem 子组件
  const tabs = React.Children.toArray(children).filter(
    child => child.type === Tab.Item
  );
  
  return (
    <div>
      <div className="tab-header">
        {tabs.map((tab, index) => (
          <button
            key={index}
            onClick={() => setActiveIndex(index)}
          >
            {tab.props.title}
          </button>
        ))}
      </div>
      <div className="tab-content">
        {tabs[activeIndex]}
      </div>
    </div>
  );
}

Tab.Item = function TabItem({ children }) {
  return <div className="tab-pane">{children}</div>;
};

// 使用
<Tab>
  <Tab.Item title="个人资料">个人资料内容</Tab.Item>
  <Tab.Item title="账号设置">账号设置内容</Tab.Item>
</Tab>

6. 常见坑点

1. 组件命名规范

  • 组件名必须大写开头,否则会被当作原生标签
  • 文件名通常跟组件名一致(UserCard.jsx

2. Props 解构

// ❌ 不要这样
function Button(props) {
  return <button className={props.className}>{props.text}</button>;
}

// ✅ 解构写法更清晰
function Button({ className, text, onClick }) {
  return <button className={className} onClick={onClick}>{text}</button>;
}

3. 条件渲染的几种方式

function Greeting({ isLoggedIn, name }) {
  // 方法1:if 语句
  if (isLoggedIn) {
    return <h1>欢迎回来,{name}!</h1>;
  }
  return <h1>请登录</h1>;
  
  // 方法2:三元运算符
  return <h1>{isLoggedIn ? `欢迎回来,${name}` : '请登录'}</h1>;
  
  // 方法3:逻辑与 &&
  return <div>{isLoggedIn && <h1>欢迎回来,{name}!</h1>}</div>;
}

4. 列表渲染的 key

// ✅ 正确:使用唯一且稳定的 key
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

// ❌ 错误:不要用 index 作为 key(除非列表静态不变)
// ❌ 不要用随机数作为 key(每次渲染都变化)

5. 组件拆分原则

  • 单一职责:一个组件只做一件事
  • 不要太大:超过 200 行考虑拆分
  • 可复用性:相同的 UI 逻辑抽成组件
  • 关注点分离:逻辑和 UI 适当分离

7. 实战:一个完整的组件示例

把上面的知识整合起来,写一个实用的评论区组件:

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';

// 主组件:评论区
function CommentSection({ postId, maxComments = 10 }) {
  const [comments, setComments] = useState([]);
  const [loading, setLoading] = useState(true);
  const [newComment, setNewComment] = useState('');
  
  // 加载评论
  useEffect(() => {
    fetchComments(postId);
  }, [postId]);
  
  const fetchComments = async (id) => {
    setLoading(true);
    try {
      const res = await fetch(`/api/posts/${id}/comments`);
      const data = await res.json();
      setComments(data.slice(0, maxComments));
    } catch (error) {
      console.error('加载评论失败:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!newComment.trim()) return;
    
    // 模拟提交评论
    const comment = {
      id: Date.now(),
      text: newComment,
      author: '当前用户',
      date: new Date().toLocaleString()
    };
    
    setComments([comment, ...comments]);
    setNewComment('');
  };
  
  return (
    <div className="comment-section">
      <h3>评论 ({comments.length})</h3>
      
      {/* 发表评论表单 */}
      <CommentForm 
        value={newComment}
        onChange={setNewComment}
        onSubmit={handleSubmit}
      />
      
      {/* 评论列表 */}
      {loading ? (
        <LoadingSpinner />
      ) : (
        <CommentList comments={comments} />
      )}
    </div>
  );
}

// 子组件:评论表单
function CommentForm({ value, onChange, onSubmit }) {
  return (
    <form onSubmit={onSubmit} className="comment-form">
      <textarea
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder="写下你的评论..."
        rows="3"
      />
      <button type="submit" disabled={!value.trim()}>
        发表评论
      </button>
    </form>
  );
}

// 子组件:评论列表
function CommentList({ comments }) {
  if (comments.length === 0) {
    return <p className="no-comments">暂无评论,快来抢沙发~</p>;
  }
  
  return (
    <div className="comment-list">
      {comments.map(comment => (
        <CommentItem key={comment.id} comment={comment} />
      ))}
    </div>
  );
}

// 子组件:单个评论
function CommentItem({ comment }) {
  return (
    <div className="comment-item">
      <div className="comment-header">
        <span className="comment-author">{comment.author}</span>
        <span className="comment-date">{comment.date}</span>
      </div>
      <p className="comment-text">{comment.text}</p>
    </div>
  );
}

// 子组件:加载动画
function LoadingSpinner() {
  return <div className="spinner">加载中...</div>;
}

// Props 类型检查
CommentSection.propTypes = {
  postId: PropTypes.string.isRequired,
  maxComments: PropTypes.number
};

CommentForm.propTypes = {
  value: PropTypes.string.isRequired,
  onChange: PropTypes.func.isRequired,
  onSubmit: PropTypes.func.isRequired
};

CommentList.propTypes = {
  comments: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
      author: PropTypes.string,
      text: PropTypes.string,
      date: PropTypes.string
    })
  )
};

export default CommentSection;
  • 组件是 React 的基石,掌握它就能搭建任何 UI
  • 函数组件 + Hooks 是现代 React 的标准写法
  • Props 是外部输入,State 是内部状态
  • 组件通信:props(父子)、状态提升(兄弟)、Context(跨级)
  • 进阶模式:高阶组件、Render Props、复合组件
  • 最佳实践:单一职责、合理拆分、命名规范

三、 React 更新驱动机制

React 是一个声明式 UI 库,我们只需要关心数据(state/props/context),当数据变化时,React 会自动更新界面。这个从“数据变化”到“界面更新”的过程,就是更新驱动

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>点我</button>
    </div>
  );
}

当你点击按钮调用 setCount,React 会:

  1. 检测到数据变化
  2. 重新渲染组件
  3. 更新 DOM

1. 更新驱动的核心流程

数据变化 → 调度更新 → render渲染阶段 → commit提交阶段 → DOM更新
   ↑                                          ↓
   └────────── 等待下一次交互 ────────────────┘

2. 🧠 触发更新的几种方式

  1. useState 的 setter
const [state, setState] = useState(initialState);
setState(newState); // 触发更新
  1. useReducer 的 dispatch
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'INCREMENT' }); // 触发更新
  1. props 变化

父组件重新渲染时,传递给子组件的 props 变化,子组件会更新。

  1. context 变化
const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={theme}>
      <ChildComponent />
      <button onClick={() => setTheme('dark')}>切换主题</button>
    </ThemeContext.Provider>
  );
}

theme 变化时,所有消费 ThemeContext 的组件都会更新。

  1. forceUpdate(类组件)
// 类组件中
this.forceUpdate(); // 强制重新渲染
  1. 根组件渲染
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />); // 首次渲染和后续更新

3. 🔄 更新的核心机制:Fiber 架构

Fiber 是 React 16 引入的新的协调引擎,它的核心目标是实现增量渲染(把渲染任务拆分成小单元,分散到多个帧中执行)。

Fiber 节点结构(简化版)

{
  tag: WorkTag, // 组件类型(函数组件、类组件、原生标签等)
  key: string | null,
  elementType: any, // 元素类型
  type: any, // 函数组件/类组件本身,或原生标签的字符串
  stateNode: any, // 对应的真实 DOM 节点或组件实例
  
  // 链表结构
  return: Fiber | null, // 父节点
  child: Fiber | null,  // 第一个子节点
  sibling: Fiber | null, // 下一个兄弟节点
  
  // 数据
  pendingProps: any, // 新的 props
  memoizedProps: any, // 上一次的 props
  memoizedState: any, // 上一次的 state
  
  // 更新队列
  updateQueue: any,
  
  // 副作用
  effects: [],
  
  // 优先级
  lanes: Lanes,
  childLanes: Lanes,
  
  // 双缓存
  alternate: Fiber | null // 指向 workInProgress 树中对应的节点
}

Fiber 使用链表结构代替了传统的递归,使得渲染可以中断和恢复。

        Root
         |
       App fiber
         |
      div fiber
      /       \
 h1 fiber    p fiber
      \
   span fiber

链表结构让 React 可以:

  • 中断:当浏览器需要处理更高优先级的事件时,暂停当前工作
  • 恢复:从中断的地方继续执行
  • 复用:比较新旧两棵树,找出需要更新的部分

4. ⚡ 更新驱动的完整流程

  1. 触发更新(Trigger)

当调用 setStatedispatch 时,React 会创建一个更新对象:

// setState 内部大致实现
function dispatchSetAction(fiber, queue, action) {
  // 创建更新对象
  const update = {
    lane, // 优先级
    action, // 要执行的 action
    next: null // 指向下一个更新
  };
  
  // 把更新添加到队列
  enqueueUpdate(fiber, queue, update);
  
  // 调度更新
  scheduleUpdateOnFiber(fiber, lane);
}
  1. 调度更新(Schedule)

React 会根据更新的优先级来决定何时开始渲染:

// scheduleUpdateOnFiber 简化版
function scheduleUpdateOnFiber(fiber, lane) {
  // 标记 fiber 有更新
  const root = markUpdateLaneFromFiberToRoot(fiber);
  
  if (root === null) return null;
  
  // 确保根节点被调度
  ensureRootIsScheduled(root);
}

优先级机制

  • Immediate:最高优先级,同步执行
  • UserBlocking:用户交互(点击、输入)
  • Normal:普通更新
  • Low:低优先级
  • Idle:空闲时执行
  1. 渲染阶段(Render Phase)- 可中断

这是 React 构建 workInProgress 树 的阶段,可以被打断。

// render 阶段的主循环
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

// 处理一个工作单元
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  
  // 开始处理当前 fiber
  const next = beginWork(current, unitOfWork, renderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 如果没有子节点,处理兄弟节点或返回父节点
    completeUnitOfWork(unitOfWork);
  } else {
    // 继续处理子节点
    workInProgress = next;
  }
}

beginWork 的核心逻辑

function beginWork(current, workInProgress, renderLanes) {
  // 根据 fiber 类型执行不同的更新逻辑
  switch (workInProgress.tag) {
    case FunctionComponent: {
      return updateFunctionComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        renderLanes
      );
    }
    case ClassComponent: {
      return updateClassComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        renderLanes
      );
    }
    case HostComponent: // 原生标签(div、span 等)
      return updateHostComponent(current, workInProgress, renderLanes);
    // ... 其他类型
  }
}
  1. 提交阶段(Commit Phase)- 不可中断

当 workInProgress 树构建完成后,进入提交阶段,这个阶段是同步执行的。

function commitRoot(root) {
  // 获取待处理的副作用
  const finishedWork = root.finishedWork;
  
  // 1. 执行前置突变(Before Mutation)
  commitBeforeMutationEffects(finishedWork);
  
  // 2. 执行突变(Mutation)- 操作 DOM
  commitMutationEffects(finishedWork);
  
  // 3. 将 workInProgress 树设为 current 树
  root.current = finishedWork;
  
  // 4. 执行布局副作用(Layout)
  commitLayoutEffects(finishedWork);
}

提交阶段的三个主要步骤

① Before Mutation

  • 调用 getSnapshotBeforeUpdate(类组件)
  • 调度 useEffect

② Mutation

  • 递归处理副作用
  • 插入、更新、删除 DOM 节点
  • 卸载 refs
  • 调用 componentWillUnmount
function commitMutationEffectsOnFiber(finishedWork) {
  switch (finishedWork.tag) {
    case HostComponent: {
      // 更新 DOM 属性
      updateDOMProperties(
        finishedWork.stateNode,
        finishedWork.memoizedProps,
        finishedWork.pendingProps
      );
      return;
    }
    // ... 其他类型
  }
}

③ Layout

  • 调用 componentDidMount/componentDidUpdate
  • 调用 useLayoutEffect 的回调
  • 赋值 refs

5. 🎨 更新类型详解

  1. 批量更新(Batching)

React 会自动批处理多个状态更新,避免不必要的渲染。

function handleClick() {
  // 在 React 事件处理函数中,这两个 setState 会被批处理
  setCount(c => c + 1);
  setFlag(f => !f);
  // 只会触发一次重新渲染
}

// 在异步代码中,React 18 之前不会批处理
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React 17:两次渲染
  // React 18:一次渲染(自动批处理)
}, 1000);

React 18 的自动批处理

// React 18 中,以下场景都会自动批处理
Promise.resolve().then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 一次渲染
});

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 一次渲染
}, 1000);
  1. 并发更新(Concurrent Updates)

React 18 引入了并发特性,可以让更新被中断,让高优先级更新先执行。

// 使用 startTransition 标记低优先级更新
import { startTransition } from 'react';

function handleChange(input) {
  // 高优先级更新
  setInputValue(input);
  
  // 低优先级更新
  startTransition(() => {
    setSearchQuery(input);
  });
}
  1. 紧急更新 vs 过渡更新
// 紧急更新:直接使用 setState
setInputValue(e.target.value); // 用户输入需要立即响应

// 过渡更新:包装在 transition 中
startTransition(() => {
  setSearchResults(query); // 搜索结果可以延迟
});

6. ⚙️ 性能优化策略

  1. 减少不必要的更新

React.memo:浅比较 props,避免不必要的重渲染

const MemoizedComponent = React.memo(MyComponent);

useMemo:缓存计算结果

const expensiveValue = useMemo(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

useCallback:缓存函数引用

const handleClick = useCallback(() => {
  doSomething(a, b);
}, [a, b]);
  1. 跳过更新

shouldComponentUpdate(类组件):

shouldComponentUpdate(nextProps, nextState) {
  // 返回 false 跳过更新
  return nextProps.id !== this.props.id;
}

useMemo 跳过子组件更新

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // 只有当 name 变化时才重新创建这个对象
  const userInfo = useMemo(() => ({ name }), [name]);
  
  return (
    <div>
      <Child data={userInfo} />
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
    </div>
  );
}
  1. 更新优先级

useDeferredValue:延迟更新

import { useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SearchResults query={deferredQuery} />
    </>
  );
}

useTransition:标记过渡更新

import { useTransition } from 'react';

function App() {
  const [isPending, startTransition] = useTransition();
  const [tabs, setTabs] = useState([]);
  
  const handleTabChange = (tab) => {
    startTransition(() => {
      setTabs(tab);
    });
  };
  
  return (
    <div>
      {isPending && <Spinner />}
      <TabContent tabs={tabs} />
    </div>
  );
}
版本架构更新特点调度
React 15Stack Reconciler递归更新,不可中断同步
React 16Fiber增量渲染,可中断优先级调度
React 17Fiber + 改进更好的批量更新兼容性改进
React 18Concurrent Fiber并发特性,自动批处理完整并发调度

更新驱动的核心要点:

  1. 触发源:setState、dispatch、props、context、forceUpdate
  2. 调度中心:根据优先级决定何时开始渲染
  3. 构建新树:可中断的 render 阶段,构建 workInProgress 树
  4. 提交更新:同步执行的 commit 阶段,操作 DOM
  5. 优化手段:memo、useMemo、useCallback、并发特性