一、React 框架核心
1. 🌟JSX
JSX 是 JavaScript XML 的缩写,它允许我们在 JavaScript 里直接写类似 HTML 的标签。
const element = <h1 className="title">Hello, React!</h1>;
看着像模板语言,但它其实是 JavaScript 的语法扩展,最终会被编译成普通的 JS 函数调用。
- 为啥要用 JSX 🤔?
- 声明式 UI:一眼就能看出页面长啥样,代码即 UI。
- 逻辑与结构融合:可以在标签里用
{}嵌入 JS 表达式,动态渲染数据。 - 避免手写繁琐的创建元素代码
- 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➡️classNamefor➡️htmlFortabindex➡️tabIndex- 行内样式要传对象:
style={{ color: 'red', fontSize: '16px' }}
-
支持嵌套
可以像 HTML 一样自由嵌套子元素:return ( <div> <header>头部</header> <main>内容</main> </div> );
- 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 的常见用法
- 基础用法:充当占位符
最常用的场景就是做布局组件,比如卡片、模态框、弹窗等,让外部决定具体内容。
function Modal({ children, isOpen }) {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>
);
}
- 多个 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>
- 传递额外的 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 这个不透明的数据结构。
- React.Children.map
安全的遍历 children,不用担心它是单个元素还是数组。
React.Children.map(children, (child, index) => {
// 对每个 child 进行处理
return <li>{child}</li>;
})
- React.Children.forEach
跟 map 类似,但不返回新数组,适合做副作用操作。
React.Children.forEach(children, child => {
console.log(child.type); // 打印每个子元素的类型
});
- React.Children.count
统计子元素的数量,包括文本节点。
function ItemsCount({ children }) {
return <div>共有 {React.Children.count(children)} 个项目</div>;
}
- React.Children.only
确保只有一个子元素,否则抛错。
function MustHaveOneChild({ children }) {
const child = React.Children.only(children);
return <div className="wrapper">{child}</div>;
}
- React.Children.toArray
将 children 转换成扁平数组,方便操作。
function ReverseChildren({ children }) {
const childrenArray = React.Children.toArray(children);
return <div>{childrenArray.reverse()}</div>;
}
4. 🚀 children 的高级玩法
- 函数作为 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>
- 条件渲染 children
根据某些条件决定是否渲染 children。
function AuthGuard({ isLoggedIn, children }) {
if (!isLoggedIn) {
return <div>请先登录</div>;
}
return children;
}
- 过滤/筛选 children
只渲染符合特定条件的子元素。
function OnlyEven({ children }) {
return (
<div>
{React.Children.map(children, (child, index) => {
// 只保留偶数索引的子元素(0,2,4...)
if (index % 2 === 0) {
return child;
}
return null;
})}
</div>
);
}
- 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. ⚠️ 常见坑点与注意事项
- 不要直接修改 children
children 是只读的,直接修改会导致不可预测的问题。
// ❌ 错误
function BadComponent({ children }) {
children.type = 'div'; // 不要这样!
return children;
}
// ✅ 正确:用 cloneElement 创建新元素
function GoodComponent({ children }) {
return React.cloneElement(children, { className: 'wrapper' });
}
- 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>;
}
- falsy 值会被渲染?
注意:0 会被渲染,false/null/undefined/true 不会。
// 页面会显示 0
<div>
{0 && <p>不会显示</p>}
</div>
// 正确写法
<div>
{count > 0 && <p>显示内容</p>}
</div>
- 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 原则:
- 不能直接修改:要用
setState或setCount - 更新可能是异步的: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 节点暴露给父组件,只想暴露特定的方法,比如 focus、scrollTo等。这时候就需要 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 会:
- 检测到数据变化
- 重新渲染组件
- 更新 DOM
1. 更新驱动的核心流程
数据变化 → 调度更新 → render渲染阶段 → commit提交阶段 → DOM更新
↑ ↓
└────────── 等待下一次交互 ────────────────┘
2. 🧠 触发更新的几种方式
- useState 的 setter
const [state, setState] = useState(initialState);
setState(newState); // 触发更新
- useReducer 的 dispatch
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: 'INCREMENT' }); // 触发更新
- props 变化
父组件重新渲染时,传递给子组件的 props 变化,子组件会更新。
- 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 的组件都会更新。
- forceUpdate(类组件)
// 类组件中
this.forceUpdate(); // 强制重新渲染
- 根组件渲染
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. ⚡ 更新驱动的完整流程
- 触发更新(Trigger)
当调用 setState 或 dispatch 时,React 会创建一个更新对象:
// setState 内部大致实现
function dispatchSetAction(fiber, queue, action) {
// 创建更新对象
const update = {
lane, // 优先级
action, // 要执行的 action
next: null // 指向下一个更新
};
// 把更新添加到队列
enqueueUpdate(fiber, queue, update);
// 调度更新
scheduleUpdateOnFiber(fiber, lane);
}
- 调度更新(Schedule)
React 会根据更新的优先级来决定何时开始渲染:
// scheduleUpdateOnFiber 简化版
function scheduleUpdateOnFiber(fiber, lane) {
// 标记 fiber 有更新
const root = markUpdateLaneFromFiberToRoot(fiber);
if (root === null) return null;
// 确保根节点被调度
ensureRootIsScheduled(root);
}
优先级机制:
- Immediate:最高优先级,同步执行
- UserBlocking:用户交互(点击、输入)
- Normal:普通更新
- Low:低优先级
- Idle:空闲时执行
- 渲染阶段(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);
// ... 其他类型
}
}
- 提交阶段(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. 🎨 更新类型详解
- 批量更新(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);
- 并发更新(Concurrent Updates)
React 18 引入了并发特性,可以让更新被中断,让高优先级更新先执行。
// 使用 startTransition 标记低优先级更新
import { startTransition } from 'react';
function handleChange(input) {
// 高优先级更新
setInputValue(input);
// 低优先级更新
startTransition(() => {
setSearchQuery(input);
});
}
- 紧急更新 vs 过渡更新
// 紧急更新:直接使用 setState
setInputValue(e.target.value); // 用户输入需要立即响应
// 过渡更新:包装在 transition 中
startTransition(() => {
setSearchResults(query); // 搜索结果可以延迟
});
6. ⚙️ 性能优化策略
- 减少不必要的更新
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]);
- 跳过更新
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>
);
}
- 更新优先级
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 15 | Stack Reconciler | 递归更新,不可中断 | 同步 |
| React 16 | Fiber | 增量渲染,可中断 | 优先级调度 |
| React 17 | Fiber + 改进 | 更好的批量更新 | 兼容性改进 |
| React 18 | Concurrent Fiber | 并发特性,自动批处理 | 完整并发调度 |
更新驱动的核心要点:
- 触发源:setState、dispatch、props、context、forceUpdate
- 调度中心:根据优先级决定何时开始渲染
- 构建新树:可中断的 render 阶段,构建 workInProgress 树
- 提交更新:同步执行的 commit 阶段,操作 DOM
- 优化手段:memo、useMemo、useCallback、并发特性