UI 表达
| 模板语法 (``) JSX(在 JS 中写 HTML)
二、JSX 与组件#在 Vue 里,HTML、JS、CSS 泾渭分明。在 React 里,一切皆 JS。
- 没有指令:没有
v-if,用 JS 的 && 或三元运算符;没有 v-for,用数组的 .map()。
- Props 是只读的:Vue 里可用
v-model 双向绑定,React 里必须手动传递 value 和 onChange 回调。
对比示例#在 Vue 3 中可能会这样写一个列表: <li v-for="item in items" :key="item.id">{{ item.name }}</li>
在 React 中,等价写法如下: function MyComponent({ items, showList }) { {showList && items.map(item => ( <li key={item.id}>{item.name}</li>
三、状态与渲染:快照与批处理#3.1 从 useState 开始#在 Vue 3 中常用 ref(0) 创建计数器;在 React 中则使用 useState。 思考: 若在 React 中直接写 let count = 0; 并在点击事件里执行 count++,页面会发生变化吗?为什么?(提示:函数组件每次更新都会重新执行。) 在 Vue 3 中,组件的 setup 里的逻辑通常只在挂载时运行一次,剩下的更新交给响应式系统。但在 React 中,渲染(Render)本质上就是调用一次函数。 3.2 核心概念:快照(Snapshot)#可以把 React 的每一次渲染想象成一张照片:
- 触发更新:当状态(State)改变时。
- 重新执行函数:React 会再次运行该组件函数。
- 生成新照片:函数根据当前的状态,返回一套新的 JSX。
- 对比与提交:React 把「新照片」和「旧照片」对比(Diffing),只把变化的部分更新到真实 DOM。
3.3 关键区别:闭包与「旧值」# const [count, setCount] = useState(0); const handleClick = () => { 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 = () => {
点击后 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>
会转换成类似: props: { className: 'active', children: 'Hello' }
操作 JS 对象比操作真实 DOM 快几个数量级。 4.2 Diffing 的两个假设#
- 同层比较:只比较同一层级的节点。若
<div> 变成 <section>,React 会直接丢弃旧分支并重新创建。
- 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 语法#
- 第一个参数:要执行的逻辑。
- 第二个参数:依赖数组,决定何时重新执行。
5.2 对应 Vue 的思维#
Vue
| React
|
`onMounted(() => { ... })`
| 依赖数组为 `[]`
|
`watch(count, () => { ... })`
| 依赖数组为 `[count]`
|
清理逻辑
| 在 effect 中 `return () => { ... }`
|
模拟 onMounted: console.log('只在挂载时运行一次');
模拟 watch: console.log('当 count 变化时运行');
模拟 onUnmounted(清理): const timer = setInterval(() => {}, 1000);
5.3 闭包陷阱#useEffect 的回调会「捕捉」某次渲染时的变量。例如:
const [count, setCount] = useState(0); const timer = setInterval(() => { console.log(count); // 永远是 0 return () => clearInterval(timer);
若要在定时器里用最新 count:要么把 count 放进依赖 [count](会频繁重置定时器),要么在定时器里用函数式更新 setCount(c => c + 1)(推荐)。 5.4 数据请求与竞态(AbortController)#当 userId 变化时请求用户数据,且要在 userId 再次变化时取消旧请求: const controller = new AbortController(); const signal = controller.signal; fetchUserData(userId, { signal }) .then(data => console.log('获取成功:', data)) if (err.name === 'AbortError') { return () => controller.abort();
理解「订阅与清理」的对称性,是掌握 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, const handleResize = () => { width: window.innerWidth, height: window.innerHeight, window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); const { width } = useWindowSize(); return <div>当前窗口宽度: {width}px</div>;
6.2 Hooks 两条规则#
- 只在最顶层调用 Hooks:不要在循环、条件或嵌套函数里调用,否则调用顺序错乱,状态会错位。
- 只在 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); const fetchData = async () => { const res = await axios({ method: apiConfig.method || 'get', return { data, loading, error };
注意:若在组件里写 useFetch({ url: '/api/user' }),每次渲染都会生成新对象引用,导致依赖变化、重复请求。应把配置提到组件外或用 useMemo 稳定引用。
七、Context:跨组件通信#在 Vue 3 中有 provide / inject;在 React 中对应 Context API,用于避免 Props Drilling。 7.1 三个角色#
- Context 对象:
React.createContext()
- Provider:在父组件外层提供数据
- useContext:子组件消费数据
7.2 主题切换示例#export const ThemeContext = createContext(); export function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); const toggleTheme = () => setTheme(prev => (prev === 'light' ? 'dark' : 'light')); <ThemeContext.Provider value={{ theme, toggleTheme }}> const { theme, toggleTheme } = useContext(ThemeContext); <button onClick={toggleTheme} style={{ background: theme === 'light' ? '#fff' : '#333' }}>
7.3 注意「穿透更新」#React 中,一旦 Provider 的 value 变化,所有使用 useContext(ThemeContext) 的组件都会重新渲染。建议按业务拆成多个小 Context(如 UserContext、CartContext),而不是一个巨型 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(() => {
8.3 useMemo#const expensiveValue = useMemo(() => { return performanceHeavyTask(data);
8.4 实战:列表 + 删除#const UserList = React.memo(({ users, onDelete }) => { <button onClick={() => onDelete(u.id)}>删除</button> 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)); <button onClick={() => setCount(c => c + 1)}>增加计数</button> <UserList users={users} onDelete={handleDelete} />
何时不用:子组件是简单 HTML 或未用 memo 时,不必强行 useCallback/useMemo,否则反而增加心智负担和 Hooks 开销。
九、状态管理进阶:Zustand#当应用变大,useContext 容易变成嵌套地狱,且一改全量更新。Zustand 类似 Pinia:无模板代码、不依赖 Context、支持按状态切片订阅。 9.1 定义 Store#import { create } from 'zustand'; const useUserStore = create((set) => ({ setUsers: (newUsers) => set({ users: newUsers }), fetchUsers: async (query) => { const res = await fetch(`https://api.github.com/search/users?q=${query}`); const data = await res.json(); set({ users: data.items || [], loading: false });
9.2 在组件中按需订阅# const users = useUserStore(state => state.users); const loading = useUserStore(state => state.loading); if (loading) return <div>加载中...</div>; {users.map(user => <li key={user.id}>{user.login}</li>)}
只有 users 或 loading 真正变化时,对应使用到它们的组件才会重绘。 9.3 选择器(Selector)与不可变性#
useUserStore(state => state.users) 表示「只关心 users」。Zustand 用引用相等判断是否变化。
- React 约定不可变更新:改状态要替换对象/数组(如
set({ users: [...state.users, newUser] })),不能 state.users.push(newUser) 再 set({ users: state.users }),否则引用不变,React 不会更新。
错误写法: state.users.push(newUser); return { users: state.users }; // 引用未变,可能不触发渲染
正确写法: users: [...state.users, newUser],
9.4 持久化(persist)#import { persist } from 'zustand/middleware'; const useAuthStore = create( login: () => set({ isLoggedIn: true }),
9.5 多字段订阅(shallow)#import { shallow } from 'zustand/shallow'; const { users, loading } = useUserStore( (state) => ({ users: state.users, loading: state.loading }),
十、服务端状态: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(); <QueryClientProvider client={queryClient}>
10.2 useQuery 替代手写 useFetch#const { data, isLoading, error } = useQuery({ queryKey: ['githubUsers', searchTerm], if (!searchTerm) return []; const res = await axios.get(`https://api.github.com/search/users?q=${searchTerm}`); staleTime: 1000 * 60 * 5, // 5 分钟内视为新鲜
优势:Stale-While-Revalidate(先展示缓存再后台更新)、自动重试、预取等。建议配合 DevTools 观察每个 queryKey 的状态。
十一、路由与样式:React Router 与 Tailwind#11.1 React Router 6.x:Loader 数据预加载#在进入路由前就拉取数据,避免「先进页面再请求」的白屏: const router = createBrowserRouter([ loader: async ({ params }) => fetch(`/api/user/${params.id}`), const userData = useLoaderData(); return <div>{userData.name}</div>;
11.2 样式:Tailwind CSS#React 没有 <style scoped>,样式易冲突。Tailwind 通过原子类在 JSX 中写样式,无需起类名、体积可控: function Card({ active }) { <div className={`p-4 rounded-lg ${active ? 'bg-blue-500' : 'bg-gray-100'}`}> <h2 className="text-xl font-bold text-white">进阶之路</h2>
十二、企业级架构与实战建议#12.1 推荐目录结构#├── api/ # TanStack Query 相关 ├── components/ # 复用 UI 组件(可配合 Tailwind) ├── routes/ # React Router 配置
12.2 实战目标建议#做一个带权限的后台管理系统,覆盖:
- 鉴权:登录状态用 Zustand + persist 持久化。
- 列表:TanStack Query 处理分页、搜索与缓存。
- 样式:Tailwind 搭响应式界面。
- 性能:列表页用 React.memo、useCallback 优化删除/编辑等回调。
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 实战视野。 |