重温前端框架-React

React 的由来

在 React 诞生之前,传统的前端开发方式使用 jQuery + 模板引擎 命令式编程,操作 DOM 繁琐且容易出错,页面性能和可维护性都越来越差。

开发者面临的典型问题包括:

  • 频繁的 DOM 更新 导致性能瓶颈;
  • 数据与视图不同步(UI 状态管理困难);
  • 代码复用性差

React 的诞生的初衷:

  • 把 UI 拆分为可复用的组件;
  • 让组件根据数据变化自动更新。

React 框架的 声明式UI + 组件化 + 虚拟DOM + 单向数据流 等特性,让开发者能够更直观的 UI 表达,极大降低复杂项目的开发和维护成本。

相比于传统前端开发,React 带来的不只是一个工具,而是一种前端工程化思维的转变。从 “一步步告诉计算机怎么做”,抽象到“告诉 React 我想要什么结果”,React 通过 Virtual DOM 和状态驱动,让 UI 成为状态的映射函数,从而实现了声明式、可预测的界面更新。

React 基础

强烈建议沉浸式看完 React 官方教程,建议从 React 17/18 版本上手学习,下面会简单总结一些要点。

组件

在 React 中,组件本质上是一个函数(或类),它根据输入的 props(属性) 和自身的 state(状态),描述要渲染的 UI 结构(React 元素树)。函数式可表达为 $$UI = f(props, state)$$

一个组件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';

function Profile({ style }) {
return (
<img
src="https://i.imgur.com/MK3eW3As.jpg"
alt="Katherine Johnson"
style={style}
/>
);
}

export default function Gallery() {
return (
<section>
<h1 style={{ color: "gray" }}>Amazing scientists</h1>
<Profile />
<Profile />
<Profile style={{ width: 40, height: 40 }}/>
</section>
);
}

  • import / export 分别为导入和导出语法,这样组件作为一个 ES6 module 可以在文件之间互相引用。
  • 组件的输入:用于使用方定制属性。
  • 组件的输出:使用 JSX 表达的声明式 UI。React 框架会将其转换为虚拟 DOM 节点,进一步转换为真实的 DOM 片段,插入到页面里。

两种不同的组件写法

函数组件(推荐)

以函数的方式声明组件,跟普通函数的区别:

  • 必须大写
  • 必须返回JSX
  • 只有一个参数props
  • 可以使用 React Hooks
  • 消灭了 this 和“实例”:每次执行是一次函数调用,跑完即销毁,状态(State)并不存在于函数内部,而是被 React 偷偷存在了外部的 Fiber 节点上(memoizedState)
  • 不能有副作用,函数组件的函数体被称为 Render 阶段。在函数体里不能直接发起网络请求、直接修改外部变量、直接操作真实 DOM。副作用必须放进 useEffect 里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 每次执行都是一次快照
// 思考:count应该是变化的?那为什么用const声明?
// 真正变化的应该是Fiber维护的count,而每次执行函数组件时,Counter里都会从Fiber拿到一个最新的count变量,它在这次执行渲染时是不会变化的。🙋:我万一在函数体写setCount呢?(首先,不推荐🚫)其次,setCount后React会记录到updateQueue里,本次执行阶段count依然不会变!
// 所以,延迟 3 秒打印的结果应该是最初执行Counter的闭包的那个count值
function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
// 延迟 3 秒打印
setTimeout(() => {
console.log('Function打印:', count);
}, 3000);
};

return <button onClick={handleClick}>点击</button>;
}

函数组件执行的时机:

  • State发生改变(setState)
  • Context的消费子组件
  • 父组件重新渲染连坐机制

⚠️ 你的潜意识:UI里面没有变量变化,就不会重新渲染

声明了 State 但 JSX 没用到,为什么也会触发组件和 Effect 执行?

  • 这个坑的本质在于:React 是一个非常“瞎”且“笨”的框架,它不做依赖收集。
  • 在 Vue 里: 框架在编译时会去扫描你的 (相当于 JSX),如果发现某个响应式变量没在视图里用到,它变了也不会触发视图更新。
  • 在 React 里: JSX 仅仅是一个普通函数的 return 返回值。 当你在函数里调用了 setState 时,React 的底层逻辑是极其生硬的:“你调了我的 API,那我就把你的函数重新执行一遍。” React 根本不在乎、也不会去提前检查你的 return 里到底写了什么。

Class 组件

以 class 的形式声明组件,继承 React.Component,通过 render 方法返回 UI 视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 有this和实例,所以这里延迟 3 秒执行打印的结果,应该是实时的count变量值
class Counter extends React.Component {
state = { count: 0 };

handleClick = () => {
// 延迟 3 秒打印
setTimeout(() => {
console.log('Class打印:', this.state.count);
}, 3000);
};

render() {
return <button onClick={this.handleClick}>点击</button>;
}
}

为什么函数组件执行完即毁?类声明组件却常驻内存了?因为React底层调用的方式不同,函数组件就是普通函数,执行完如果没有闭包就全部被 GC 清理掉了。类声明是用new关键字 js 实例化了,在内存里硬生生地开辟了一块空间,自然有this。

对比点 类组件 函数组件
写法 复杂(class、this) 简洁(纯函数)
状态管理 this.state / setState useState
生命周期 componentDidMount 等 useEffect
逻辑复用 HOC / Render Props 自定义 Hook
性能 较低 更优(Hooks + Fiber)
未来趋势 被弱化 推荐标准

**重要:**推荐使用函数组件,结合 react hooks,可以做到更极致的代码复用。React 团队也表示未来会弱化类组件支持(不会立刻废弃,但已停止扩展)。

从组件到页面

通过不断复用和组合不同的组件,来形成最终的页面。

一个典型页面与其对应的抽象的 React 组件:

认识 JSX

在表达 UI 视图时,React 提供了 createElement 方法来创建一个视图元素 Element。但是 UI 视图本身会有比较复杂的嵌套和组合,直接使用 createElement 会导致代码可读性很差。

React 引入 JSX 来扩展 JS 与法,让 UI 视图的表达形式与原生 HTML 近似,极大提升代码可读性。

JSX 语法速览

语法 说明 示例
基本用法 JSX 是在 JavaScript 中编写类似 HTML 的语法。 JavaScript<br>const element = <h1>Hello, world!</h1>; <br>
表达式插值 使用 {} 在 JSX 中嵌入任意 JS 表达式。 JavaScript<br>const user = {name: "abc"};<br>const element = <h1>{user.name}</h1>;<br>
属性传值(Props) 属性值为字符串时用引号,表达式用 {}。 JavaScript<br>const element = <img src={user.avatar} alt="头像" />;<br>
条件渲染 使用三元表达式或逻辑与(&&)进行条件渲染。 JavaScript<br> <br>const element = (<br> <div><br> {isLogin ? <User /> : <Login />} <br> {count > 0 && <span>{count}</span>}<br> </div><br>)<br>
列表渲染 使用 map() 遍历数组并返回元素,需加唯一 key。【参考 JavaScript<br>const elements = (<br> <ul><br> {items.map(item => <br> <li key={item.id}>{item.name}</li><br> )}<br> </ul><br>)<br>
Fragment(片段) 用 <></> 包裹多个子元素而不引入额外 DOM。 JavaScript<br>const element = (<br> <><br> <h1>标题</h1><br> <p>内容</p><br> </><br>)<br>

组件 Props & State

Props 和 State 都能够让组件呈现出不同的形态。但从设计意图上,两者存在较大差异。

理解组件 Props

Props 可以理解为组件的属性,组件向外暴露这些参数用于定制

这个概念可以与 HTML 标签的属性对应上,组件可以视为自定义的 HTML 标签,组件的属性用法也类似于 HTML 标签的属性。

Props 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Avatar from './Avatar.js';

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

export default function Profile(props) {
return (
<Card>
<Avatar size={100} {...props} />
</Card>
);
}
  • 函数的第一个参数为函数组件的 Props,可以直接通过 props 获取,或者解构获取内部属性
  • JSX 嵌套作为 children 传入,children默认属性
  • 可以使用 spread 语法传入 Props
  • 函数 Props 由父组件传入,不能在组件内部进行修改
  • 默认情况下,当 Props 发生变化时,组件会重新渲染

理解组件 State

**State 表示组件内部的状态,可以视为组件的 “记忆”。**比如 Todo List 的列表数据,表单提交中的状态,输入框中的文本,表格的过滤条件等。

在一些用户交互后,内部状态会随着更新,更新状态会自动触发组件重新渲染。

1
2
3
4
5
6
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
);
}
  • 在函数组件里使用 useState hook 来声明一个组件状态
  • 在 class component 里使用 state 和 setState 来管理组件状态

Props vs State

  • Props 代表这个组件对外暴露的可定制接口。在设计组件时,应当提前考虑组件如何被调用,从而来设计组件的 Props。
  • State 是组件内部的记忆。保存用户在与组件交互过程中,产生的一些中间状态。

State 管理

初始化状态

1
const [state, setState] = useState(initialState)

更新状态

使用 useState 返回结果里的第二个参数 setState 来更新状态。

1
2
setState(newState); // 直接更新 state
setState(state => newState); // 根据之前的 state 更新 state

上述两种模式的差异:

  • 直接更新 state
    在 React 底层,组件是一个 Fiber 对象。 这个 Fiber 对象上有两个极其重要的属性: memoizedState 和 updateQueue。当你调用 setState 时,React 不会立刻去改 memoizedState,而是把你的请求包装成一个 Update 对象,塞进 updateQueue 这个链表里。这就是所谓的“批处理(Batching)”。最后,React遍历链表,然后去渲染DOM
1
2
3
4
5
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
}
  • 根据之前的 state 更新 state
1
2
3
4
5
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}

React 状态更新队列

  • legacy 模式

在 React 中,状态更新(setState 或 useState 的 setter)不是立即生效的,它会被放入一个 更新队列(Update Queue) 中,等到合适的时机(如事件结束或下一次渲染周期)再统一执行。
如果每次调用 setState() 都立即更新组件和重新渲染,那在一次点击中多次调用 setState() 会导致 重复渲染,性能非常差。
React 会将这些更新暂存(enqueue),然后在合适的时机 批量处理(flush),只渲染一次组件。

  • concurrent 模式

setState 不再是立即同步更新,而是触发一个带有「优先级」的更新任务,React 的调度器会根据优先级决定 何时、是否中断、是否恢复 该更新。

在哪里更新状态

  • 事件处理函数
1
2
3
4
5
6
7
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<button onClick={handleClick}>Clicked {count} times</button>
);
}
  • 【不推荐,参考】useEffect 里
1
2
3
4
5
6
7
8
9
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
  • 【不推荐】函数组件渲染期间直接调整 state

    相比 useEffect 较优,但仍不推荐。 示例里应当移除掉 selection 状态,直接在渲染期间计算。

1
2
3
4
5
6
7
8
9
10
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
}

更新复杂对象状态

  • 直接更新
    正确更新复杂对象状态,原则是需要更新对象地址(构建新的对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});

person.artwork.city = 'New Delhi';
// ❌ 无法生效,因为React是浅比较(地址)
setPerson(person);

// ✅ 可以生效
setPerson({
...person,
artwork: {
...person.artwork,
city: 'New Delhi'
}
});
  • 使用 Immer 更新
    本质上也是依赖 immutable 库,构建出新的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useImmer } from 'use-immer';
const [person, setPerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});

// ✅ 可以生效
setPerson(draft => {
draft.artwork.city = 'Lagos';
});

外部如何更新状态

原则上讲,不能在组件外部直接更新组件内的状态。如果想要在外部更新组件内的一个状态,可以有以下方式:

  • ✅ 提升子组件的状态到共同的父组件中,将状态作为属性传递给子组件
  • ✅ 使用全局状态 Context / Redux / Zustand 等
  • 通过 ref 暴露内部更新方法(forwardRef + useImperativeHandle)【合法但最佳实践不推荐】
  • 全局发布订阅机制
  • ❌ 直接暴露 setState 【强烈不推荐】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { useState, useRef, forwardRef, useImperativeHandle } from 'react';

// ================= 子组件 =================
// 1. 用 forwardRef 包裹子组件,让它能够接收父组件传进来的 ref
const Counter = forwardRef((props, ref) => {
const [count, setCount] = useState(0);

// 2. 核心操作:拦截 ref,自定义要暴露给父组件的对象(遥控器按键)
useImperativeHandle(ref, () => {
// 这个 return 返回的对象,就是父组件将来拿到的 ref.current
return {
// 暴露一个重置内部状态的方法
reset: () => {
setCount(0);
},
// 甚至可以暴露一个读取内部状态的方法
getCount: () => {
return count;
}
};
});

return (
<div style={{ border: '1px solid black', padding: '10px' }}>
<h3>子组件的数字: {count}</h3>
<button onClick={() => setCount(count + 1)}>子组件内部的 +1 按钮</button>
</div>
);
});

// ================= 父组件 =================
export default function Parent() {
// 1. 创建一个空白的 ref
const counterRef = useRef(null);

return (
<div style={{ padding: '20px' }}>
{/* 2. 把 ref 像插管一样插进子组件 */}
<Counter ref={counterRef} />

<hr />
{/* 3. 通过 ref.current 调用子组件暴露出来的方法 */}
<button onClick={() => counterRef.current.reset()}>
【父组件控制】强制清零子组件
</button>
<button onClick={() => alert(`当前数字是: ${counterRef.current.getCount()}`)}>
【父组件控制】读取子组件数据
</button>
</div>
);
}

单向数据流

在 React 中,数据只能由上而下(从父组件到子组件)流动,组件不能直接修改父组件或兄弟组件的状态。

  • 父组件通过 props 把数据传给子组件;
  • 子组件通过 回调函数 通知父组件改变;
  • React 的 UI 渲染永远是由状态(state)→ UI 的单向映射。
  • 组件的状态管理变得可预测、可复现、可调试。

与单向数据流相对应的,即双向数据流。双向数据流在一些其他前端框架里有使用到,比如 Angular/ Vue。

  • 单向数据流:数据只能从上层往下层流动,状态更可控,适用于大型复杂项目
  • 双向数据流:视图与数据双向绑定,用起来会比较方便,但在大型项目里维护难度较大。

⚠️ 注意,其实Vue底层也是单向数据流,只不过v-model语法糖帮我们自动声明了响应函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// React 写法
function App() {
const [text, setText] = useState('');
return (
<div>
{/*
1. value={text}:数据流向 UI(向下)
2. onChange={...}:UI 触发事件通知数据改变(向上)
*/}
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>你输入了:{text}</p>
</div>
);
}

<!-- Vue 写法 -->
<template>
<div>
<!-- 只需要一个 v-model,搞定一切 -->
<input v-model="text" />
<p>你输入了:{{ text }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>

<!-- 撕掉 v-model 语法糖后的真实面目 -->
<input
:value="text"
@input="event => text = event.target.value"
/>

Hooks

Hooks 是一组可以让你在函数组件中使用状态(state)和生命周期(life cycle)等 React 特性的函数。

在 Hooks 出现之前:

  • 逻辑复用困难(需要用 HOC 或 render props);
  • 组件状态逻辑分散(生命周期函数里混杂逻辑);
  • 类组件 this 指向问题频繁;
  • 代码臃肿、难测试。

React 团队提出 Hooks 的目的:
✅ 更好的逻辑复用方式(通过自定义 Hook);
✅ 让函数组件更强大(能使用 state、effect 等);
✅ 更纯粹的组件模型(UI = f(state))。

常用 Hooks

  • useState 管理组件内部状态
1
2
3
4
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>点击 {count}</button>;
}

⚠️ 执行时机:函数组件执行时顺势完成

业务需求 React Hook 生产环境真实场景
“页面上要有弹窗开关 / 输入框能打字 / 列表能翻页” useState 只要是用户交互后,界面上肉眼可见会变化的东西,全部塞进 State。
  • useEffect 副作用(生命周期)处理
1
2
3
4
5
6
7
8
useEffect(() => {
// deps 发生变化时拦截,浅比较
}, [deps])

useEffect(() => {
console.log('组件挂载');
return () => console.log('组件卸载');
}, []); // 依赖数组为空,只在挂载/卸载时执行

忘掉 Vue 里的 watch!useEffect 不是监听,而是拦截!
⚠️ 执行时机:

  • 执行组件函数(Render): 拿到虚拟 DOM,顺便完成 deps 的浅比较,发现变化了,登记“稍后执行此副作用”。(React 无形的手会帮我们拿到之前的旧值做浅比较)
  • 计算并挂载(Commit): React 把虚拟 DOM 变成真实 DOM 的增量更新指令。
  • 交出主线程(Paint): React 极其自觉地停止工作,把浏览器的 JavaScript 主线程让给浏览器的渲染引擎。渲染引擎开始计算 CSS,并最终把像素画到屏幕上。
  • 大戏开场(Effect): 屏幕画完后,浏览器空闲下来了。React 通过内部的任务调度器(底层用的是 MessageChannel 宏任务),把刚才登记的那个副作用塞进执行队列。
    • 动作 A: 如果你上次渲染留下了一个 return () => {…} 的清理函数,先执行它(打扫旧战场)。清理函数只在卸载或者更新阶段执行,初次挂载不执行。
    • 动作 B: 执行你这次传进来的新的 useEffect 回调函数。
业务需求 React Hook 生产环境真实场景
“进页面要拉取后端数据 / 发送埋点日志” useEffect + [] 和外部世界(服务器端)通讯。只在初次进页面跑一次。然后设置state重新渲染。不用useEffect直接把请求写函数组件里然后改state,你小子看看小心死循环

类比 Vue 的 onMounted
“用户选了城市后,要重新拉取这个城市的门店数据” useEffect + [cityId] 联动刷新。当某个特定条件(cityId)变化时,自动触发副作用。
“离开这个页面时,把还在跑的定时器关掉” useEffect 里的 return 善后清理。防止内存泄漏或幽灵 Bug。

类比 Vue 的 onUnMounted
“商品总价 = 单价 × 数量,数量变了总价跟着变” 普通的 const 变量计算 ⚠️ 绝佳避坑点! 只要能用现有 State 算出来的数据,绝对不要再存一个新 State,直接在组件里写 const total = price * num 即可。
“点按钮要把输入框拉起键盘(获取焦点)” useRef 绕过 React 数据流,直接暴力抓取真实的 DOM 元素。
“我要存一个定时器的 ID,但不想触发页面刷新” useRef useRef 不仅能抓 DOM,还能当一个**“静默的储物箱”**。存进去的东西变了,绝不会触发重新渲染。
  • useMemo 缓存计算结果
1
2
const value = useMemo(() => expensiveCalc(), [deps])
// 根据 deps 变化自动计算出新的值

⚠️ 执行时机:函数组件执行时顺势完成,包括deps的浅比较

  • useCallback
    缓存函数引用
    避免函数重新创建,触发组件属性变更,导致组件重新渲染
1
2
3
4
5
const handleClick = useCallback(() => {
// ...
}, [deps])

return <Button onClick={handleClick}>+</Button>

⚠️ 执行时机:函数组件执行时顺势完成,包括deps的浅比较

  • Tips:避免函数重新创建,如果不涉及组件props或state,可以踢到外面。如果你非要state,那就useCallback吧!依赖可以是[]

  • useContext 使用上下文(全局数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { createContext, useContext, useState } from 'react';

// 1. 创建 Context(仓库)
const ThemeContext = createContext('light'); // 没写Provider的情况下消费者拿到的退而求其次的值,他与你未来Provider的value值类型应该是一类的

function App() {
const [theme, setTheme] = useState('light');

return (
// 2. 提供值(在老爸组件里把仓库装满)
<ThemeContext.Provider value={theme}>
// 或:<ThemeContext.Provider value={{theme, setTheme}}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
切换模式
</button>
<Toolbar />
</ThemeContext.Provider>
);
}

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

// 3. 使用值(在深层子组件里直接拿,不需要经过中间商 Toolbar)
function ThemedButton() {
const theme = useContext(ThemeContext); // 直接获取数据
// 或:const {theme, setTheme} = useContext(ThemeContext);
const style = {
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
padding: '10px'
};

return <button style={style}>当前主题是:{theme}</button>;
}
  • ⚠️ 执行时机:函数组件执行时顺势完成的。当 React 运行你的组件函数时,执行到 useContext(MyContext) 这一行,它会立即去查找当前 Fiber 节点向上最近的 Provider,并立刻返回那个 Context 的最新值。
业务需求 React 实际
“整个 App 都要用的东西(用户信息、白天/黑夜主题)” useContext / Redux / Zustand 跨越组件层级共享的数据,也就是所谓的“全局状态”。
  • useRef 保存可变引用或访问 DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 访问 DOM
const listRef = useRef<HTMLDivElement>(null);
useEffect(() => {
listRef.current?.scrollTo({
top: listRef.current.scrollHeight,
behavior: "smooth",
});
}, [listRef.current?.scrollHeight]);

return (
<>
<list ref={listRef}></list>
</>
)
1
2
3
4
5
6
7
8
9
10
// 保存不需要被渲染的变量
// Websocket创建的实例
const wsRef = useRef(null);
useEffect(() => {
wsRef.current = new WebSocket("xxx");
// ...
return () => {
wsRef.current && wsRef.current.close();
}
}, []);
  • 执行时机:顺势完成(DOM情况下,ref 的绑定和识别在DOM真正挂载或更新时完成的,即commit阶段)

Hooks 使用规则

React 用“Hook 调用顺序”作为内部状态匹配机制,如果_多次渲染之间顺序变化_会导致状态错乱(React 会直接报错)。

使用时需遵循一下规则:

  • 只能在函数组件或自定义 Hook 中使用:不能在普通函数或 if/for 语句中用
  • 每次渲染 Hook 调用顺序必须一致:React 依靠调用顺序来关联状态
  • 自定义 Hook 必须以 use 开头:如 useFetch, useUser 等

React 与 Vue 的闭包

JavaScript 的函数在声明时(即使还没有被押入函数执行栈执行),它的内部属性 [[Environment]] 就已经打包并记住了它所处环境的“根变量引用”(即该变量在内存中的格子地址),而不是具体的某个值。

  • 在 Vue 中:组件从头到尾只执行一次,响应式数据(ref)的根变量引用(对象的内存地址)永恒不变。改变的只是对象肚子里的属性(.value)。因此,函数即使一辈子不重新声明,每次执行时顺着当年记下的“根变量地址”找过去,都能摸到最新的属性值。

  • 在 React 中:数据的每一次修改,都会导致组件函数重新整行执行。由于不可变数据原则,新一帧的 state 是一个全新开辟的根变量引用。如果你不把变量写进 Hook 的 [] 依赖里,你的老函数在执行时,顺着它当年记下的“老根变量地址”找过去,找到的只能是活在过去那一帧的旧快照。

1
2
3
4
5
6
7
8
9
10
11
12
const [num, setNum] = useState(40); 
const [loading, setLoading] = useState(false); 
// 💡 推荐写法:把所有用到的响应式根变量都放进依赖数组 

const handleCalculate = useCallback(() => 

if (loading) return
console.log("发射最新的值:", num); 
// 每次 num 或 loading 变了,React 会生成新函数绑定新变量 
workerRef.current?.postMessage(num);
}, [num, loading]);
// 注意不能写成[],不然函数实际执行时拿到的num和loading应该不是最新的

useCallback, useMemo, useEffect 等需要依赖的 hook 声明时都要注意这一点

自定义 Hook

赋予 React 代码逻辑复用的能力。把复杂逻辑提炼为 Hook,让 UI 组件更纯粹。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return width;
}

// 使用
function App() {
const width = useWindowWidth();
return <p>窗口宽度:{width}</p>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function useDocumentVisibility(): VisibilityState {
const [documentVisibility, setDocumentVisibility] = useState(() => getVisibility());

useEventListener(
'visibilitychange',
() => {
setDocumentVisibility(getVisibility());
},
{
target: () => document,
},
);

return documentVisibility;
}

Hooks 原理浅析

React 会在渲染时:

  • 为每个组件维护一个 Hook 链表;
  • 按照 Hook 调用的顺序保存或读取相应的状态;
  • 在下一次渲染时,根据这个顺序取出上一次的状态进行更新。

Hook 链表及其类型定义:

1
2
const [name, setName] = useState('John');
const [age, setAge] = useState(18);
1
2
3
4
5
6
7
8
9
10
11
12
{
memoizedState: 'John',
baseState: 'John',
baseQueue: null,
queue: null,
next: {
memoizedState: 18,
baseState: 18,
baseQueue: null,
queue: null,
},
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// react-reconciler/src/ReactFiberHooks.old.js
// 对应 useState / useRef / useMemo
export type Hook = {|
memoizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any, // 待更新的操作 updateQueue
next: Hook | null,
|};

// 专门对应 useEffect
export type Effect = {|
tag: HookFlags,
create: () => (() => void) | void, // useEffect 的回调函数
destroy: (() => void) | void, // useEffect return 的函数
deps: Array<mixed> | null,
next: Effect,
|};

React 渲染函数组件:

  • 全局指针指向当前 Fiber 节点,在 renderWithHooks 里执行组件函数
  • 组件首次渲染时,调用每个 Hook(如 useState/useEffect),创建初始的 Hook 数据;
  • 调用 mountWorkInProgressHook 将函数组件的 Hook 转换成链表(如上所示);
  • 组件更新时,会按 Hook 调用顺序,从存储在 Fiber 的 state 里获取 Hook 节点,更新对应的值。
    这也是为什么组件多次渲染期间 Hook 的调用顺序必须保持一致

事件机制

React 并没有使用原生的浏览器事件机制,而是封装了一套合成事件(SyntheticEvent)机制。
![[Pasted image 20260522225425.png]]
即我们在 React 代码里绑定的 onClick,onChange 等事件回调,并不会直接绑定到目标元素上,而是绑定到 React 根节点上。

当事件触发时找到触发元素,依次向上冒泡事件,执行回调函数。

这么做的好处是:

  • 屏蔽浏览器差异
  • 事件委托机制,减少内存消耗 (根结点统一监听事件)

虚拟 DOM

React 的 虚拟 DOM(Virtual DOM) 是 React 性能优化的核心机制之一。它的主要作用是提高 UI 更新效率,使页面在频繁更新时依然保持高性能。

虚拟 DOM 是一种 以 JavaScript 对象形式 表示真实 DOM 结构的轻量级副本。

  • 虚拟 DOM
    • 保存在内存中;
    • 可以快速比较前后版本的变化;
    • 最终只把“必要的最小改动”同步到真实 DOM。
1
2
3
4
5
6
7
8
9
10
11
12
const vdom = {
type: 'div',
props: {
id: 'app',
children: {
type: 'h1',
props: {
children: 'Hello React'
}
}
}
};
  • 直接操作真实 DOM
    • 每次修改都会引发重绘(repaint)和回流(reflow)
    • 浏览器需要频繁更新渲染树,性能消耗大。
1
2
3
<div id="app">
<h1>Hello React</h1>
</div>

React 的更新流程一般是这样的:

  1. 初次渲染
  • React 组件生成虚拟 DOM。
  • React 将虚拟 DOM 转换为真实 DOM,并插入页面。
  1. 状态更新
  2. 当组件的 state 或 props 变化时,React 生成一个新的虚拟 DOM(新的快照)。
  3. Diff 算法
  4. 对比新旧虚拟 DOM,找出变化的部分(比如哪个节点、属性或文本变了)。
  5. 批量更新真实 DOM
  6. 只更新需要变化的部分,而不是重新渲染整个页面。

虚拟 DOM Diff 算法

比较两棵树的所有节点(完全递归对比)是 O(n³) 的复杂度。 React 通过一系列假设和优化,将原本 复杂度降到了 O(n)

  • 同层比较:只比较相同层级的节点;
  • 相同组件只更新 props,不同类型节点直接替换;
  • 列表 Diff: 根据 key 建立哈希表,来判断哪些子节点移动、修改或删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div className="box">
<Header title="旧标题" />
<ul>
<li key="A">苹果</li>
<li key="B">香蕉</li>
</ul>
<p>我是底部</p>
</div>

<div className="box">
<Header title="新标题" /> {/* 变化1:相同标签,只是属性变了,更新 props */}
<ul>
<li key="C">橘子</li> {/* 列表Diff2:新增了橘子,且在开头 */}
<li key="A">苹果</li> {/* 列表Diff3:苹果没变,但位置变了 */}
{/* 列表Diff4:香蕉被删了 */}
</ul>
<footer>我是底部</footer> {/* 变化5:标签名变了(不同标签组件,直接暴力删除替换) */}
</div>

状态管理进阶

复杂应用下的问题

随着应用的体量变大,简单的使用 useState 来管理状态会出现以下问题:

状态分散、难以维护 每个组件都维护自己的局部状态,数据需要一层层传递,一旦结构复杂或数据依赖改变,就很容易失控。

如果只用 state:

- 每个组件都维护自己的局部状态;

- 数据需要在父子组件之间一层层传递(props drilling);

- 当状态被多个页面或模块依赖时,数据同步变得困难。
状态同步困难、逻辑重复 不同模块可能依赖同一份状态(例如:用户信息、权限、购物车等)。

仅用 state 时:

- 每个模块都需要单独拉取数据或维护逻辑;

- 状态变更需要多处同步更新;

- 逻辑重复、容易出现数据不一致。
性能问题(频繁重新渲染) 当顶层组件管理过多状态时:

- 状态变更会触发整个子树重新渲染;

- 即使某个子组件不依赖该状态,也会被迫更新。

可以使用 React Context 或者全局状态管理库来解决这些问题。

React Context

React 内部的跨组件共享状态机制,能够跨组件共享状态

用法实例

1
2
3
4
5
6
7
8
9
10
11
// 状态提供方
const ThemeContext = createContext();

function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout />
</ThemeContext.Provider>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 状态消费者
function DetailPage() {
const { theme } = useContext(ThemeContext);
return <div className={theme}>Click</div>;
}

function ThemeToggle() {
const { theme, setTheme } = useContext(ThemeContext);
const toggleTheme = () => {
setTheme(t => t === 'light' ? 'dark' : 'light')
}
return <Button onClick={toggleTheme}>theme</Button>;
}

局限性

  • Context 更新会导致所有消费者重渲染

React Context 是“广播式”的:当 Provider 的 value 变化时,所有 useContext() 的组件都会重新渲染 —— 即使它们只用到 value 的一部分。

  • 缺乏状态逻辑管理能力

状态管理库一般能够处理异步数据流,支持中间件机制,管理状态变更历史等,这些都是 React Context 无法做到的。

适用场景

轻量全局状态(theme、locale、userInfo)/ 简单只读状态

状态管理库

大型 React 应用,一般会采用状态管理库来统一管理状态。社区里也存在各式各样的开源方案可用,从原理上大概可以分为三类范式。

状态管理范式

  1. Flux

代表库如 Redux、Zustand。

  • Flux 的核心原则是**数据应该沿单一方向流动,**这使得应用程序的逻辑更易于预测和理解。
  • Action动作 -> Dispatcher 调度器 -> Store 状态存储 -> UI 视图
  • Action 携带 payload 数据,经由 Dispatcher 更新到 Store。UI 层订阅了 Store 的变化,会随着 Store 的变化自动更新视图。
  1. Atomic State

代表库如 Recoil、Jotai。

  • 核心思想:将状态切成很多小的 “原子”(atoms)——每个 atom 是状态的最小单元,可被读/写。然后还有 Selectors(派生状态)——基于 atom 或其它 selector 计算得来。
  • 保留了一定的单向数据流,但更强调 “细粒度” 状态管理、组件订阅特定状态、以及 derived state。
  1. Proxy State

代表库如 Valtio、MobX。

  • 利用了 js 的 Proxy 或类似机制,实现对状态对象的“代理”监控,从而实现响应式(reactivity)——状态更改后,自动触发 UI 更新。
  • 相对比 Flux 那种强结构,Proxy 模式更「直觉」,更少样板代码。在大型应用复杂度上来后,管理会比较乱。

Redux

Redux 是一个用于可预测和可维护的全局状态管理的 JS 库。

核心思想

与 Flux 一致,在其基础上做了一定的简化。

  • 单一数据源:应用程序的全局状态存储在单个 Store 中。
  • **状态只读:**改变状态只能通过发出一个 Action 来触发。
  • 通过纯函数变更:即 Reducer,本质上是纯函数,它接收前一个状态和一个 action,并返回下一个状态。

This content is only supported in a Feishu Docs

最小化示例(Redux Toolkit)

Redux 本身设计非常精简。导致手动初始化 Redux 配置,直到能够正常处理前端状态及副作用的过程是比较复杂的。

推荐使用 Redux Toolkit 来帮助配置,以及使用 React Redux 来与 React 视图层进行连接。

相比传统 Redux,Redux Toolkit 做了什么

https://redux-toolkit.js.org/introduction/why-rtk-is-redux-today#how-redux-toolkit-is-different-from-the-redux-core

  • 初始化 Store
1
2
3
4
5
6
7
8
9
import { combineSlices, configureStore } from "@reduxjs/toolkit"
import { counterSlice } from "../features/counter/counterSlice"
import { quotesApiSlice } from "../features/quotes/quotesApiSlice"

const rootReducer = combineSlices(counterSlice, quotesApiSlice)

export const store = configureStore({
reducer: rootReducer,
})
  • 包裹 Provider
1
2
3
4
5
6
7
8
9
10
11
12
import { Provider } from "react-redux"
import { store } from "./store"

const root = createRoot(container)

root.render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
)
  • 定义状态分片 Slice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { asyncThunkCreator, buildCreateSlice } from "@reduxjs/toolkit"

export const createAppSlice = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator },
})

const initialState = {
value: 0,
status: "idle",
}

export const counterSlice = createAppSlice({
name: "counter",
initialState,
reducers: create => ({
incrementByAmount: create.reducer(
(state, action) => {
state.value += action.payload
},
),
incrementAsync: create.asyncThunk(
async (amount: number) => {
const response = await fetchCount(amount)
return response.data
},
{
pending: state => {
state.status = "loading"
},
fulfilled: (state, action) => {
state.status = "idle"
state.value += action.payload
},
rejected: state => {
state.status = "failed"
},
},
),
}),
})
  • 连接视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useDispatch, useSelector } from "react-redux"
import { incrementAsync, incrementByAmount } from "./counterSlice"

export const Counter = (): JSX.Element => {
const dispatch = useDispatch()
const count = useSelector(state => state.count)

return (
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(3))}
>
Add Amount
</button>
)
}

异步请求处理(RTK Query)

RTK Query 从“管理状态”转向“管理缓存数据”。开发者定义数据来源与失效策略,框架完成请求生命周期、缓存、挂载/卸载时机管理与 hooks 生成。

  • 使用 RTK Query 请求 API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'

export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (build) => ({
getPokemonByName: build.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})

export const { useGetPokemonByNameQuery } = pokemonApi
  • 更新 store config
1
2
3
4
5
6
7
8
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
})
  • 调用 API
1
2
3
export const App = () => {
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
}

Zustand

Zustand 是一个“更近组件、订阅更细、结构不强制”的状态管理方案。它把“选择器 + 订阅”放在一等公民位置,让你只在真正相关的状态变化时重渲染,减少无谓开销。

核心思想

沿袭了 Flux 的核心:单向数据流。组件触发动作(调用 set 或封装的 action),更新 store;选择器提取片段;订阅者按需重渲染。它去掉了繁琐的“必须是 action + reducer”的显式结构,把“怎么组织”交给开发者。

![[Pasted image 20260523001933.png]]

最小化示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { create } from 'zustand'

// 类型定义
type CounterState = {
count: number
increment: () => void
reset: () => void
}

// 创建 store(作为 hook 使用)
export const useCounterStore = create<CounterState>(((set) => ({
count: 0,
otherState: "test",
increment: () => set((s) => ({ count: s.count + 1 })),
reset: () => set({ count: 0 }),
})))

// 组件按需订阅
function Counter() {
const count = useCounterStore((s) => s.count)
const increment = useCounterStore((s) => s.increment)
return (
<div>
<span>Count: {count}</span>
<button onClick={increment}>+1</button>
</div>
)
}

异步请求处理

不同于 Redux,Zustand 里处理异步请求,无需引入单独的异步请求框架或者范式。直接在 store 里定义 async function 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const useCounterStore = create<CounterState>(((set) => ({
count: 0,
loading: false,
error: null,
increment: () => set((s) => ({ count: s.count + 1 })),
reset: () => set({ count: 0 }),
fetchRemoteCount: async () => {
set({ loading: true })
try {
const res = await fetch(`xxxx`)
const data = await res.json()
set({ user: data, loading: false })
} catch (err) {
set({ error: err.message, loading: false })
}
}
})))

最佳实践

  • 按领域划分 Store,如 ThemeNotificationWizardForm
  • 复杂对象需要构建新的结构体来更新,或者使用 Immer
  • 使用 useShallow 来阻止不必要的 rerender

路由管理

传统应用切换页面时,都需要重新加载 html,整体体验比较差。

React 应用里一般会采用路由管理工具来在 SPA 应用里实现多页面切换的能力。

SPA 的本质都是监听路由变化(hashchange/popchange),通过 JS 来动态将对应 Page 的 HTML 片段插入到当前页面的某个节点(Container)下。

目前最主流的 React 路由库是 React Router

用法概括如下:

  • 路由接入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import About from "./pages/About";

function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<User />} />
</Routes>
</BrowserRouter>
);
}

export default App;
  • 路由跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Link } from "react-router-dom";

function Nav() {
return (
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</nav>
);
}

function Login() {
const navigate = useNavigate();

function handleLogin() {
// 登录逻辑...
navigate("/"); // 跳转到首页
}

return <button onClick={handleLogin}>登录</button>;
}
  • 路由参数
1
2
3
4
5
6
7
8
9
// 定义路由参数,正则规范
<Route path="/user/:id" element={<User />} />

// 获取路由参数
import { useParams } from "react-router-dom";
function User() {
const { id } = useParams();
return <h1>用户 ID: {id}</h1>;
}
  • 路由嵌套
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Route path="/dashboard" element={<Dashboard />}>
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>

import { Outlet } from "react-router-dom";

function Dashboard() {
return (
<div>
<Sidebar />
<Outlet /> {/* 子路由组件会渲染在这里 */}
</div>
);
}

一般情况下使用 BrowserRouter,基于 HTML5 History API。路由更新后,触发 popupstate 事件,React-Router 获取 path 后进行 match,然后执行渲染逻辑。

样式管理

在 React 应用里,引入 CSS 存在多种方式。

方案对比

直接引用 CSS 文件

1
2
3
4
5
6
// 直接引用 CSS 文件
import "./App.css"

function App() {
return <div className="container">Hello</div>;
}

优点:

  • 简单容易理解

缺点:

  • 类名容易冲突
  • 样式是全局作用的
  • 难以做组件化封装

CSS Modules

1
2
3
4
5
6
// CSS Modules:以模块形式引入
import styles from "./styles.module.css"

function App() {
return <div className={styles.container}>Hello</div>;
}

优点:

  • 组件级样式隔离
  • 结合 classnames 库动态控制
  • 无类名冲突

缺点:

  • 本地开发通过类名定位代码比较麻烦(已有插件解决)
  • 组件被引用后,如果需要样式定制无法直接使用 CSS 修改。推荐通过组件属性进行定制。

CSS In JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import styled from "styled-components";

const Button = styled.button`
background: ${(props) => (props.primary ? "blue" : "gray")};
color: white;
border-radius: 5px;
padding: 8px 12px;
`;

function App() {
return (
<>
<Button>普通按钮</Button>
<Button primary>主要按钮</Button>
</>
);
}

优点:

  • 能够在 JS 里直接编写样式
  • 样式与组件强绑定
  • 无类名冲突

缺点:

  • 编译时性能略低
  • 大量组件时,运行时样式注入开销略大
  • 同时存在 CSS Modules 存在的部分缺点

原子化 CSS 框架

1
2
3
4
5
6
7
function App() {
return (
<div className="p-4 bg-blue-500 text-white rounded-lg shadow-md">
Hello Tailwind
</div>
);
}

优点:

  • 不再需要写独立的样式文件
  • 原子化、组件化
  • 构建时优化性能

缺点:

  • 上手难度高
  • 类名多时可读性较差
  • 自定义复杂样式需要额外配置

推荐的方式

在开发 React 应用时,一般推荐结合 UI 组件库使用,设计上遵循组件库的 UI 风格,尽可能避免自定义样式

无可避免自定义样式时,通过 CSS Modules 的方式进行定制

常见 React 应用设计原则

这个章节将介绍几种常用的设计原则,来让我们写出模块化、低耦合、可维护、可扩展、性能良好的 React 应用。

遵循原子设计模式

原子设计的五个不同层次:原子 > 分子 > 有机体 > 模板 > 页面,与 React 的组件化架构完美契合。

在设计和组织 React 组件时,也可以按类似的维度进行划分:原子组件 > 基础组件 > 复合组件 > 业务容器组件 > 应用层

业务逻辑与 UI 渲染分开

将“获取/计算数据 + 业务逻辑”与“UI 渲染”分开,让一个容器组件负责 fetch API、管理 loading/error 状态,然后将数据传递给一个单纯负责渲染的展示组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UserList.jsx —— 展示组件 (Presentational Component)
import React from "react";

export const UserList = ({ users, loading, error }) => {
if (loading) return <p>Loading users...</p>;
if (error) return <p style={{ color: "red" }}>❌ {error}</p>;

return (
<ul>
{users.map((user) => (
<li key={user.id}>
👤 {user.name} ({user.email})
</li>
))}
</ul>
);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// UserListContainer.jsx —— 容器组件 (Container Component)
import React, { useEffect, useState } from "react";
import { UserList } from "./UserList";

export const UserListContainer = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchUsers = async () => {
// ...
};

fetchUsers();
}, []);

// 把数据和状态交给展示组件
return <UserList users={users} loading={loading} error={error} />;
};

使用自定义 Hook 复用业务逻辑

利用自定义 Hook 将可复用的“状态逻辑 / 副作用逻辑”抽离出来,让组件代码更加整洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// useUsers.js
import { useEffect, useState } from "react";

export function useUsers() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchUsers = async () => {
// ...
};

fetchUsers();
}, []);

// ✅ 返回数据与状态,让外部组件使用
return { users, loading, error };
}
1
2
3
4
5
6
7
8
9
10
11
// UserListContainer.jsx —— 容器组件 (Container Component)
import React from "react";
import { UserList } from "./UserList";
import { useUsers } from "./useUsers";

export const UserListContainer = () => {
const { users, loading, error } = useUsers();

// 把数据和状态交给展示组件
return <UserList users={users} loading={loading} error={error} />;
};

使用 Provider 管理全局状态

使用 Context + Provider 来管理全局状态,避免“prop drilling”(组件层层传递 prop)的问题。

引入状态管理库管理复杂状态

对于跨组件交互频繁、状态逻辑复杂的场景,可以引入成熟的状态管理库(如 Redux、Zustand、MobX 等)进行统一管理。

按路由延迟加载页面

利用 React.lazy() 和 Suspense 实现页面的按需加载,减小首屏体积,按页面划分代码分割点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from "react-router-dom";

const Home = lazy(() => import('@/pages/Home'));
const About = lazy(() => import('@/pages/About'));
const User = lazy(() => import('@/pages/User'));

function App() {
return (
<BrowserRouter>
<Routes>
<Suspense fallback={<Loading />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<User />} />
</Suspense>
</Routes>
</BrowserRouter>
);
}

export default App;

避免重复渲染或重复计算

合理使用 React.memo、useMemo、useCallback 来避免不必要的组件重新渲染或昂贵的数据重复计算。(⚠️ 注意搭配使用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import React, { useState, useMemo, useCallback, memo } from "react";

// 👇 子组件:UserList —— 使用 React.memo 避免不必要渲染
const UserList = memo(({ users, onSelect }) => {
console.log("👀 UserList rendered");

return (
<ul>
{users.map((user) => (
<li key={user.id} onClick={() => onSelect(user)}>
👤 {user.name}
</li>
))}
</ul>
);
});

// 👇 父组件:UserSearch
export const UserSearch = () => {
const [search, setSearch] = useState("");
const [selected, setSelected] = useState(null);

const allUsers = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
];

// ✅ useMemo: 只有当 search 改变时才重新计算过滤结果
const filteredUsers = useMemo(() => {
console.log("🔍 Filtering users...");
return allUsers.filter((u) =>
u.name.toLowerCase().includes(search.toLowerCase())
);
}, [search]);

// ✅ useCallback: 保持函数引用稳定,避免触发子组件重复渲染
const handleSelect = useCallback((user) => {
setSelected(user);
}, []);

return (
<div style={{ fontFamily: "sans-serif" }}>
<h2>🔎 User Search</h2>
<input
type="text"
value={search}
placeholder="Search user..."
onChange={(e) => setSearch(e.target.value)}
/>

<UserList users={filteredUsers} onSelect={handleSelect} />

{selected && <p>✅ Selected: {selected.name}</p>}
</div>
);
};

使用 UI 库

这一章节将讲述如何使用成熟的 UI 组件库,来加速我们的开发。课程采用 ArcoDesign 组件库进行演示。

使用示例

This content is only supported in a Feishu Docs

定制主题

ArcoDesign 使用了 Less 作为预编译语言,通过 Less 的 modifyVars 功能,可以很方便的对样式粒子变量进行定制。

  1. 从 less 文件引用样式
1
2
// global.less
import '@arco-design/web-react/dist/css/index.less';
  1. 参考 ArcoDesign components/style/theme/global.less 定制 less 变量。或者查看组件粒度变量进行覆盖components/Button/style/token.less
1
2
3
4
// theme.less
@color-text-1: #232323;

@btn-size-mini-radius: 4px;
  1. 或者在打包工具的 less-loader 里修改环境变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// webpack.config.js
module.exports = {
rules: [{
test: /\.less$/,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader',
}, {
loader: 'less-loader',
+ options: {
+ modifyVars: { // 在less-loader@6 modifyVars 配置被移到 lessOptions 中
+ 'arcoblue-6': '#f85959',
+ },
+ javascriptEnabled: true
+ },
}],
...
}],
...
}

重温前端框架-React
http://example.com/2026/06/05/重温前端框架-React/
作者
Lingkai Shi
发布于
2026年6月5日
许可协议