3565 字
18 分钟
Learn React
2026-02-19
2026-03-10
统计加载中...

Learn React#


若已熟悉 Vue 3,学习 React 会很快。Vue 3 的 Composition API(组合式 API) 在很多设计思想上都受到了 React Hooks 的启发。不过,虽然「长得像」,但两者的底层逻辑有着本质的区别。本文会分阶段系统性地切入 React 的世界。


一、核心差异:从「自动驾驶」切换到「手动挡」#

Vue 3 最大的特点是响应式(Reactivity)。当修改一个 ref 时,Vue 会自动追踪依赖,只更新需要变动的部分。

而 React 遵循的是单向数据流不可变性(Immutability)。在 React 中,当状态改变时,整个函数组件会重新执行一遍

特性
Vue 3 (Composition API)
React (Hooks)
逻辑组织
setup 函数(仅执行一次)
函数组件本身(渲染一次执行一次)
响应式原理
基于 Proxy 的数据劫持
基于快照(Snapshot)的状态更新
UI 表达
模板语法 (template)
JSX(在 JS 中写 HTML)

二、JSX 与组件#

在 Vue 里,HTML、JS、CSS 泾渭分明。在 React 里,一切皆 JS。

  • 没有指令:没有 v-if,用 JS 的 && 或三元运算符;没有 v-for,用数组的 .map()
  • Props 是只读的:Vue 里可用 v-model 双向绑定,React 里必须手动传递 valueonChange 回调。

对比示例#

在 Vue 3 中可能会这样写一个列表:

<template>
<div v-if="showList">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</div>
</template>

在 React 中,等价写法如下:

function MyComponent({ items, showList }) {
return (
<div>
{showList && items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</div>
);
}

三、状态与渲染:快照与批处理#

3.1 从 useState 开始#

在 Vue 3 中常用 ref(0) 创建计数器;在 React 中则使用 useState

思考: 若在 React 中直接写 let count = 0; 并在点击事件里执行 count++,页面会发生变化吗?为什么?(提示:函数组件每次更新都会重新执行。)

在 Vue 3 中,组件的 setup 里的逻辑通常只在挂载时运行一次,剩下的更新交给响应式系统。但在 React 中,渲染(Render)本质上就是调用一次函数

3.2 核心概念:快照(Snapshot)#

可以把 React 的每一次渲染想象成一张照片

  1. 触发更新:当状态(State)改变时。
  2. 重新执行函数:React 会再次运行该组件函数。
  3. 生成新照片:函数根据当前的状态,返回一套新的 JSX。
  4. 对比与提交:React 把「新照片」和「旧照片」对比(Diffing),只把变化的部分更新到真实 DOM。

3.3 关键区别:闭包与「旧值」#

function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 打印的是点击之前的值
};
return <button onClick={handleClick}>{count}</button>;
}

在 React 中,handleClick 里的 console.log(count) 会打印点击之前的值。因为当前的 count 被「锁定」在这次渲染的快照里。setCount 并不是直接修改当前变量,而是告诉 React:用 count + 1 作为新状态,再运行一遍这个函数

3.4 批处理(Batching)#

在 Vue 中,修改 ref.value 时 Proxy 会立即拦截并准备触发更新。而在 React 中,setCount 是向 React 提交一个「更新申请」。React 会等当前事件处理函数中的所有代码执行完后,再统一进行一次重新渲染。

3.5 连续调用的陷阱#

const [count, setCount] = useState(0);
const handleMultipleAdd = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};

点击后 count 会变成 1,而不是 3。因为三次调用时函数还未重新执行,count 始终是 0,React 最终只应用最后一次「改成 1」的请求。

解决:函数式更新

const handleMultipleAdd = () => {
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};

这样点击后 count 会正确变为 3

3.6 Commit 阶段#

当 React 算出所有状态变更后,进入 Commit(提交)阶段:对比新旧虚拟 DOM,只修改真实 DOM 中变化的部分(例如只改 textContent)。 因此虽然「函数重新执行」发生在 JS 层(Virtual DOM),真实 DOM 操作被控制在最小范围内,性能依然可以很好。


四、虚拟 DOM 与 Diffing#

在 Vue 中,响应式系统能精确知道哪个属性变了;React 则生成完整的虚拟树,通过对比找出差异。

4.1 什么是虚拟 DOM?#

虚拟 DOM 就是一个普通的 JavaScript 对象,用来描述真实 DOM 的结构。例如:

<div className="active">Hello</div>

会转换成类似:

{
type: 'div',
props: { className: 'active', children: 'Hello' }
}

操作 JS 对象比操作真实 DOM 快几个数量级。

4.2 Diffing 的两个假设#

  1. 同层比较:只比较同一层级的节点。若 <div> 变成 <section>,React 会直接丢弃旧分支并重新创建。
  2. Key 的重要性:在列表中用 key 标识稳定元素。

4.3 为什么 key 不能用 index?#

列表 [A, B, C] 在头部插入 D 时:

  • 用 index (0,1,2,3):React 会认为 index 0 从 A 变成了 D,导致不必要的更新甚至输入框错位。
  • 用唯一 ID:React 能识别 A、B、C 只是移动,只有 D 是新增,只做一次插入和移动。

4.4 性能优化「手动挡」#

  • React.memo:Props 没变则不重新渲染子组件。
  • useMemo / useCallback:缓存计算结果或函数引用,避免因父组件重绘导致不必要的子组件更新。

小结:Vue 依赖追踪、精确更新;React 快照对比、虚拟 DOM Diff,需在关键处手动优化。


五、useEffect:副作用与生命周期#

在 Vue 3 中,生命周期(onMounted)和数据监听(watch)是分开的。在 React 中,它们统一由 useEffect 处理。

5.1 语法#

useEffect(() => {
// 业务逻辑
}, [dep1, dep2]);
  • 第一个参数:要执行的逻辑。
  • 第二个参数:依赖数组,决定何时重新执行。

5.2 对应 Vue 的思维#

Vue
React
`onMounted(() => { ... })`
依赖数组为 `[]`
`watch(count, () => { ... })`
依赖数组为 `[count]`
清理逻辑
在 effect 中 `return () => { ... }`

模拟 onMounted:

useEffect(() => {
console.log('只在挂载时运行一次');
}, []);

模拟 watch:

useEffect(() => {
console.log('当 count 变化时运行');
}, [count]);

模拟 onUnmounted(清理):

useEffect(() => {
const timer = setInterval(() => {}, 1000);
return () => {
clearInterval(timer);
};
}, []);

5.3 闭包陷阱#

useEffect 的回调会「捕捉」某次渲染时的变量。例如:

const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远是 0
}, 1000);
return () => clearInterval(timer);
}, []);

若要在定时器里用最新 count:要么把 count 放进依赖 [count](会频繁重置定时器),要么在定时器里用函数式更新 setCount(c => c + 1)(推荐)。

5.4 数据请求与竞态(AbortController)#

userId 变化时请求用户数据,且要在 userId 再次变化时取消旧请求:

useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetchUserData(userId, { signal })
.then(data => console.log('获取成功:', data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被取消');
} else {
// 处理真正错误
}
});
return () => controller.abort();
}, [userId]);

理解「订阅与清理」的对称性,是掌握 React 副作用的关键。


六、自定义 Hooks(逻辑复用)#

在 Vue 3 中会写 Composables;在 React 中对应 Custom Hooks,目标一致:逻辑抽离与复用

自定义 Hook 就是use 开头的函数,内部可以调用其他 React Hooks。

6.1 useWindowSize 示例#

import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// 使用
function MyComponent() {
const { width } = useWindowSize();
return <div>当前窗口宽度: {width}px</div>;
}

6.2 Hooks 两条规则#

  1. 只在最顶层调用 Hooks:不要在循环、条件或嵌套函数里调用,否则调用顺序错乱,状态会错位。
  2. 只在 React 函数中调用:在函数组件或自定义 Hook 里用。

6.3 useFetch 封装#

异步请求必须放在 useEffect 里,否则每次渲染都会发请求,可能死循环。正确形态示例:

export function useFetch(apiConfig) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const res = await axios({
url: apiConfig.url,
method: apiConfig.method || 'get',
...apiConfig.options,
});
setData(res.data);
} catch (e) {
setError(e);
} finally {
setLoading(false);
}
};
fetchData();
}, [apiConfig.url]);
return { data, loading, error };
}

注意:若在组件里写 useFetch({ url: '/api/user' }),每次渲染都会生成新对象引用,导致依赖变化、重复请求。应把配置提到组件外或用 useMemo 稳定引用。


七、Context:跨组件通信#

在 Vue 3 中有 provide / inject;在 React 中对应 Context API,用于避免 Props Drilling

7.1 三个角色#

  1. Context 对象React.createContext()
  2. Provider:在父组件外层提供数据
  3. useContext:子组件消费数据

7.2 主题切换示例#

// 创建 Context
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// 顶层包裹
function App() {
return (
<ThemeProvider>
<Navbar />
<MainContent />
</ThemeProvider>
);
}
// 任意子组件消费
function Button() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme} style={{ background: theme === 'light' ? '#fff' : '#333' }}>
切换模式
</button>
);
}

7.3 注意「穿透更新」#

React 中,一旦 Provider 的 value 变化,所有使用 useContext(ThemeContext) 的组件都会重新渲染。建议按业务拆成多个小 Context(如 UserContextCartContext),而不是一个巨型 Context。


八、性能优化:Memoization#

父组件一变,子组件默认全重绘,需要「手动挡」优化。

  • React.memo:仅当子组件 Props 变化时才重渲染。
  • useCallback:缓存函数引用,避免传给 memo 子组件的回调每次都是新的。
  • useMemo:缓存计算结果(类似 Vue 的 computed),依赖不变就不重算。

原则:不要滥用。只有组件确实昂贵、或需要稳定引用(配合 memo)时才用。

8.1 React.memo#

const UserItem = React.memo(({ user }) => {
console.log('子组件渲染:', user.name);
return <li>{user.name}</li>;
});

8.2 useCallback#

// 每次渲染 handleClick 都是新引用,会导致 memo 失效
const handleClick = useCallback(() => {
console.log('点击了');
}, []);

8.3 useMemo#

const expensiveValue = useMemo(() => {
return performanceHeavyTask(data);
}, [data]);

8.4 实战:列表 + 删除#

const UserList = React.memo(({ users, onDelete }) => {
return (
<ul>
{users.map(u => (
<li key={u.id}>
{u.login}
<button onClick={() => onDelete(u.id)}>删除</button>
</li>
))}
</ul>
);
});
export default function App() {
const [count, setCount] = useState(0);
const [users, setUsers] = useState([...]);
const handleDelete = useCallback((id) => {
setUsers(prev => prev.filter(user => user.id !== id));
}, []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
<UserList users={users} onDelete={handleDelete} />
</div>
);
}

何时不用:子组件是简单 HTML 或未用 memo 时,不必强行 useCallback/useMemo,否则反而增加心智负担和 Hooks 开销。


九、状态管理进阶:Zustand#

当应用变大,useContext 容易变成嵌套地狱,且一改全量更新。Zustand 类似 Pinia:无模板代码、不依赖 Context、支持按状态切片订阅。

9.1 定义 Store#

import { create } from 'zustand';
const useUserStore = create((set) => ({
users: [],
loading: false,
setUsers: (newUsers) => set({ users: newUsers }),
fetchUsers: async (query) => {
set({ loading: true });
try {
const res = await fetch(`https://api.github.com/search/users?q=${query}`);
const data = await res.json();
set({ users: data.items || [], loading: false });
} catch (e) {
set({ loading: false });
}
},
}));

9.2 在组件中按需订阅#

function UserList() {
const users = useUserStore(state => state.users);
const loading = useUserStore(state => state.loading);
if (loading) return <div>加载中...</div>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.login}</li>)}
</ul>
);
}

只有 usersloading 真正变化时,对应使用到它们的组件才会重绘。

9.3 选择器(Selector)与不可变性#

  • useUserStore(state => state.users) 表示「只关心 users」。Zustand 用引用相等判断是否变化。
  • React 约定不可变更新:改状态要替换对象/数组(如 set({ users: [...state.users, newUser] })),不能 state.users.push(newUser)set({ users: state.users }),否则引用不变,React 不会更新。

错误写法:

set(state => {
state.users.push(newUser);
return { users: state.users }; // 引用未变,可能不触发渲染
});

正确写法:

set(state => ({
users: [...state.users, newUser],
}));

9.4 持久化(persist)#

import { persist } from 'zustand/middleware';
const useAuthStore = create(
persist(
(set) => ({
isLoggedIn: false,
login: () => set({ isLoggedIn: true }),
}),
{ name: 'auth-storage' }
)
);

9.5 多字段订阅(shallow)#

import { shallow } from 'zustand/shallow';
const { users, loading } = useUserStore(
(state) => ({ users: state.users, loading: state.loading }),
shallow
);

十、服务端状态:TanStack Query#

React 不负责异步数据,只负责 UI。数据可以这样区分:

  • Client State:主题、表单等 → Zustand
  • Server State:接口数据 → TanStack Query

TanStack Query 通过 queryKey 做缓存:key 不变则用缓存;key 变则重新请求。

10.1 配置 Provider#

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<SearchPage />
</QueryClientProvider>
);
}

10.2 useQuery 替代手写 useFetch#

const { data, isLoading, error } = useQuery({
queryKey: ['githubUsers', searchTerm],
queryFn: async () => {
if (!searchTerm) return [];
const res = await axios.get(`https://api.github.com/search/users?q=${searchTerm}`);
return res.data.items;
},
enabled: !!searchTerm,
staleTime: 1000 * 60 * 5, // 5 分钟内视为新鲜
});

优势:Stale-While-Revalidate(先展示缓存再后台更新)、自动重试、预取等。建议配合 DevTools 观察每个 queryKey 的状态。


十一、路由与样式:React Router 与 Tailwind#

11.1 React Router 6.x:Loader 数据预加载#

在进入路由前就拉取数据,避免「先进页面再请求」的白屏:

const router = createBrowserRouter([
{
path: '/user/:id',
element: <UserPage />,
loader: async ({ params }) => fetch(`/api/user/${params.id}`),
},
]);
function UserPage() {
const userData = useLoaderData();
return <div>{userData.name}</div>;
}

11.2 样式:Tailwind CSS#

React 没有 <style scoped>,样式易冲突。Tailwind 通过原子类在 JSX 中写样式,无需起类名、体积可控:

function Card({ active }) {
return (
<div className={`p-4 rounded-lg ${active ? 'bg-blue-500' : 'bg-gray-100'}`}>
<h2 className="text-xl font-bold text-white">进阶之路</h2>
</div>
);
}

十二、企业级架构与实战建议#

12.1 推荐目录结构#

src/
├── api/ # TanStack Query 相关
├── components/ # 复用 UI 组件(可配合 Tailwind)
├── hooks/ # 自定义 Hooks
├── store/ # Zustand
├── routes/ # React Router 配置
└── views/ # 页面级组件

12.2 实战目标建议#

做一个带权限的后台管理系统,覆盖:

  1. 鉴权:登录状态用 Zustand + persist 持久化。
  2. 列表TanStack Query 处理分页、搜索与缓存。
  3. 样式Tailwind 搭响应式界面。
  4. 性能:列表页用 React.memouseCallback 优化删除/编辑等回调。

12.3 进阶可学#

  • useReducer:多状态、强关联时统一更新逻辑。
  • HOC / Render Props:权限封装、弹窗等场景仍常见。
  • useTransition / useDeferredValue:标记非紧急更新,减轻卡顿。

总结:React 技能树总览#

层次
内容
核心原理
虚拟 DOM、Diffing、快照与批处理
Hooks
useState、useEffect、useContext、自定义 Hooks
性能
memo、useCallback、useMemo、引用与不可变性
状态
Zustand(本地)+ TanStack Query(服务端)
工程化
React Router(含 Loader)+ Tailwind CSS

这套 Zustand + TanStack Query 组合在国内大厂 React 项目中非常常见。按本文顺序把「快照思维 → Hooks → 性能 → 状态与数据请求 → 路由与样式」过一遍,即可建立起完整的 React 实战视野。