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
|
function Counter() { const [count, setCount] = useState(0);
const handleClick = () => { 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
| class Counter extends React.Component { state = { count: 0 };
handleClick = () => { 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); setState(state => newState);
|
上述两种模式的差异:
- 直接更新 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(age + 1); setAge(age + 1); }
|
1 2 3 4 5
| function handleClick() { setAge(a => a + 1); setAge(a => a + 1); setAge(a => a + 1); }
|
React 状态更新队列
在 React 中,状态更新(setState 或 useState 的 setter)不是立即生效的,它会被放入一个 更新队列(Update Queue) 中,等到合适的时机(如事件结束或下一次渲染周期)再统一执行。
如果每次调用 setState() 都立即更新组件和重新渲染,那在一次点击中多次调用 setState() 会导致 重复渲染,性能非常差。
React 会将这些更新暂存(enqueue),然后在合适的时机 批量处理(flush),只渲染一次组件。
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> ); }
|
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';
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';
const Counter = forwardRef((props, ref) => { const [count, setCount] = useState(0);
useImperativeHandle(ref, () => { 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() { 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
| 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> <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
1 2 3 4
| function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>点击 {count}</button>; }
|
⚠️ 执行时机:函数组件执行时顺势完成
| 业务需求 |
React Hook |
生产环境真实场景 |
| “页面上要有弹窗开关 / 输入框能打字 / 列表能翻页” |
useState |
只要是用户交互后,界面上肉眼可见会变化的东西,全部塞进 State。 |
1 2 3 4 5 6 7 8
| useEffect(() => {
}, [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,还能当一个**“静默的储物箱”**。存进去的东西变了,绝不会触发重新渲染。 |
1 2
| const value = useMemo(() => expensiveCalc(), [deps])
|
⚠️ 执行时机:函数组件执行时顺势完成,包括deps的浅比较
- useCallback
缓存函数引用
避免函数重新创建,触发组件属性变更,导致组件重新渲染
1 2 3 4 5
| const handleClick = useCallback(() => {
}, [deps])
return <Button onClick={handleClick}>+</Button>
|
⚠️ 执行时机:函数组件执行时顺势完成,包括deps的浅比较
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';
const ThemeContext = createContext('light');
function App() { const [theme, setTheme] = useState('light');
return ( <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 |
跨越组件层级共享的数据,也就是所谓的“全局状态”。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
|
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); workerRef.current?.postMessage(num); }, [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
|
export type Hook = {| memoizedState: any, baseState: any, baseQueue: Update<any, any> | null, queue: any, next: Hook | null, |};
export type Effect = {| tag: HookFlags, create: () => (() => void) | void, destroy: (() => void) | void, 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 的更新流程一般是这样的:
- 初次渲染
- React 组件生成虚拟 DOM。
- React 将虚拟 DOM 转换为真实 DOM,并插入页面。
- 状态更新
- 当组件的 state 或 props 变化时,React 生成一个新的虚拟 DOM(新的快照)。
- Diff 算法
- 对比新旧虚拟 DOM,找出变化的部分(比如哪个节点、属性或文本变了)。
- 批量更新真实 DOM
- 只更新需要变化的部分,而不是重新渲染整个页面。
虚拟 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>; }
|
局限性
React Context 是“广播式”的:当 Provider 的 value 变化时,所有 useContext() 的组件都会重新渲染 —— 即使它们只用到 value 的一部分。
状态管理库一般能够处理异步数据流,支持中间件机制,管理状态变更历史等,这些都是 React Context 无法做到的。
适用场景
轻量全局状态(theme、locale、userInfo)/ 简单只读状态
状态管理库
大型 React 应用,一般会采用状态管理库来统一管理状态。社区里也存在各式各样的开源方案可用,从原理上大概可以分为三类范式。
状态管理范式
- Flux
代表库如 Redux、Zustand。
- Flux 的核心原则是**数据应该沿单一方向流动,**这使得应用程序的逻辑更易于预测和理解。
- Action动作 -> Dispatcher 调度器 -> Store 状态存储 -> UI 视图
- Action 携带 payload 数据,经由 Dispatcher 更新到 Store。UI 层订阅了 Store 的变化,会随着 Store 的变化自动更新视图。
- Atomic State
代表库如 Recoil、Jotai。
- 核心思想:将状态切成很多小的 “原子”(atoms)——每个 atom 是状态的最小单元,可被读/写。然后还有 Selectors(派生状态)——基于 atom 或其它 selector 计算得来。
- 保留了一定的单向数据流,但更强调 “细粒度” 状态管理、组件订阅特定状态、以及 derived state。
- 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 本身设计非常精简。导致手动初始化 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
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, })
|
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>, )
|
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 生成。
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
|
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), })
|
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 }
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,如
Theme、Notification、WizardForm
- 复杂对象需要构建新的结构体来更新,或者使用 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
| import "./App.css"
function App() { return <div className="container">Hello</div>; }
|
优点:
缺点:
CSS Modules
1 2 3 4 5 6
| 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
| 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
| 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
| 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
| 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";
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> ); });
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" }, ];
const filteredUsers = useMemo(() => { console.log("🔍 Filtering users..."); return allUsers.filter((u) => u.name.toLowerCase().includes(search.toLowerCase()) ); }, [search]);
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 功能,可以很方便的对样式粒子变量进行定制。
- 从 less 文件引用样式
1 2
| import '@arco-design/web-react/dist/css/index.less';
|
- 参考 ArcoDesign
components/style/theme/global.less 定制 less 变量。或者查看组件粒度变量进行覆盖components/Button/style/token.less
1 2 3 4
| @color-text-1: #232323;
@btn-size-mini-radius: 4px;
|
- 或者在打包工具的 less-loader 里修改环境变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = { rules: [{ test: /\.less$/, use: [{ loader: 'style-loader', }, { loader: 'css-loader', }, { loader: 'less-loader', + options: { + modifyVars: { + 'arcoblue-6': '#f85959', + }, + javascriptEnabled: true + }, }], ... }], ... }
|