React 对照速成文档
React 对照速成文档
目标:能看懂已有的 React 项目代码。本文假设你已掌握上面的《Vue3 学习笔记》,每节都会标注"对照 Vue3",帮你快速建立映射。
阅读技巧:React 代码的核心心智模型只有一句话——UI = f(state),组件就是一个"状态 → 界面"的纯函数。
1. React 简介
- 由 Facebook(Meta)于 2013 年开源,用于构建用户界面的 JavaScript 库(注意:是"库",不是"框架",路由、状态管理等需要第三方配合)。
- 当前主流写法是 函数组件 + Hooks(2019 年 React 16.8 引入),老项目里可能残留 Class 组件,两者要都会"读"。
- 与 Vue 最大的理念差异:
- Vue:模板 + 响应式(数据变了,视图自动更新,靠
ref/reactive代理)。 - React:没有响应式,数据变了 React 会重新执行整个组件函数,重新算出新的 JSX 与旧的对比(虚拟 DOM diff),只更新变化的部分。
- Vue:模板 + 响应式(数据变了,视图自动更新,靠
对照 Vue3:Vue 的"数据变化驱动视图"是细粒度的(哪个 ref 变了只更新相关的地方);React 是"重跑函数 + diff"的粗粒度模型。读代码时看到组件函数体里大量普通变量、
return的 JSX,就是整个组件会反复执行。
2. 创建 React 工程
2.1. 基于 Vite 创建(推荐)
npm create vite@latest
# Project name: react_test
# Framework: React
# Variant: TypeScript (推荐) / JavaScript
cd react_test
npm install
npm run dev
2.2. 项目入口与结构
react_test/
├── index.html # 入口 HTML,里面有 <div id="root"></div>
├── src/
│ ├── main.tsx # 应用入口:把 App 组件渲染到 #root
│ ├── App.tsx # 根组件
│ └── ...
src/main.tsx(React 版的 main.ts):
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
// Vue3: createApp(App).mount('#app')
// React: 把 <App /> 挂载到 id 为 root 的 DOM 节点
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
对照 Vue3:
ReactDOM.createRoot(...).render(...)≈createApp(App).mount('#app')。
React.StrictMode是开发模式下的"严格模式",会让组件在开发时多执行一遍用于暴露副作用问题,读代码时忽略即可。
3. React 核心语法
3.1. 函数组件与 JSX
React 组件就是一个返回 JSX 的函数。JSX 是"在 JS 里写 HTML",本质上会被编译成 React.createElement 调用。
// App.tsx
export default function App() {
const name = '张三'
// 返回的这段"HTML"就是 JSX
return (
<div className="person">
<h2>姓名:{name}</h2>
<button>按钮</button>
</div>
)
}
读 JSX 必须记住的规则(与 Vue 模板的差异点):
| 需求 | Vue 模板 | React JSX |
|---|---|---|
| 插入变量 | {{name}} |
{name} |
| 类名 | class="app" |
className="app" |
| 行内样式 | :style="{color:'red'}" |
style={{ color: 'red' }} |
| 绑定属性 | :src="url" |
src={url} |
| 点击事件 | @click="fn" |
onClick={fn} |
| 条件渲染 | v-if / v-show |
见 3.2 |
| 列表渲染 | v-for |
见 3.3 |
| 注释 | <!-- --> |
{/* 注释 */} |
3.2. 条件渲染(对照 v-if / v-show)
React 里条件渲染就是普通的 JS 逻辑,因为 JSX 就在 JS 里。
function Demo({ flag, age }: { flag: boolean; age: number }) {
return (
<div>
{/* v-if / v-else-if:用三元表达式 */}
{age >= 18 ? <p>成年人</p> : <p>未成年人</p>}
{/* 只有 if 没有 else:用 && 短路 */}
{flag && <p>flag 为真才显示</p>}
{/* v-show:切换 style.display */}
<p style={{ display: flag ? 'block' : 'none' }}>相当于 v-show</p>
</div>
)
}
对照:
v-if="x"→{x && <div/>};v-else→ 三元表达式的后半段;v-show→ 自己控制display。
3.3. 列表渲染(对照 v-for)
React 用数组的 .map() 方法,且 key 直接写在 JSX 属性上,必须传。
const games = [
{ id: 'a01', name: '英雄联盟' },
{ id: 'a02', name: '王者荣耀' },
{ id: 'a03', name: '原神' },
]
function GameList() {
return (
<ul>
{/* v-for="g in games" :key="g.id" 的 React 写法 */}
{games.map((g) => (
<li key={g.id}>{g.name}</li>
))}
</ul>
)
}
对照:Vue 的
v-for="(item, index) in list"→ React 的list.map((item, index) => (...))。注意key在 React 里是 JSX 属性(key={g.id}),Vue 里是绑定指令(:key="g.id")。
3.4. 事件处理(对照 v-on)
React 事件名是驼峰式,直接传函数引用(不加括号),事件对象就是原生的,没有 $event 特殊语法。
function Demo() {
function handleClick(e: React.MouseEvent) {
console.log('点击了', e.target)
}
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log('输入了', e.target.value)
}
return (
<div>
<button onClick={handleClick}>点我</button>
{/* 需要传参时用箭头函数包裹 */}
<button onClick={() => handleClick2('参数')}>带参</button>
<input onChange={handleChange} />
</div>
)
}
对照:
@click="fn"→onClick={fn};@click="fn(1)"→onClick={() => fn(1)}。React 没有事件修饰符(.stop、.prevent),要手动写e.stopPropagation()、e.preventDefault()。
3.5. 表单与受控组件(对照 v-model)
React 没有 v-model。表单输入的值要手动绑定 value + 手动监听 onChange 去更新状态,这种"值由 React state 完全控制"的输入框叫受控组件。
import { useState } from 'react'
function InputDemo() {
const [text, setText] = useState('')
return (
<input
value={text} // 数据 → 页面
onChange={(e) => setText(e.target.value)} // 页面 → 数据
/>
)
}
对照:
v-model="text"≈value={text}+onChange={(e) => setText(e.target.value)}。
这正是 Vue 里v-model拆开后的底层形式(:value+@input)。
3.6. useState(对照 ref / reactive)
useState 是 React 最核心的 Hook,用于定义"组件的状态"。状态变了,组件函数重新执行。
import { useState } from 'react'
function Counter() {
// 对照:let count = ref(0)
// useState 返回 [当前值, 修改函数],通常用数组解构接收
const [count, setCount] = useState(0)
const [user, setUser] = useState({ name: '张三', age: 18 })
function add() {
setCount(count + 1) // 对照 count.value += 1
// 注意:不能写成 count = count + 1,普通变量改了不会触发重渲染
}
function changeAge() {
// 对象类型必须"整体替换",不能原地修改(与 reactive 完全不同!)
setUser({ ...user, age: user.age + 1 }) // 展开旧对象 + 覆盖要改的属性
}
return (
<div>
<h2>计数:{count}</h2>
<h2>姓名:{user.name},年龄:{user.age}</h2>
<button onClick={add}>+1</button>
<button onClick={changeAge}>年龄+1</button>
</div>
)
}
读 React 代码时最重要的规则:state 不可变(immutable)
- 基本类型:
setCount(count + 1)。 - 对象/数组:永远不要原地改(如
user.age++、arr.push()),要新建一份再 set。因为 React 靠"引用是否变了"来判断要不要重渲染。- 对象:
setUser({ ...user, age: 18 }) - 数组:
setList([...list, newItem])、setList(list.filter(...))
- 对象:
对照 Vue3:
ref(0)≈useState(0)(返回值.value变成了解构出的[值, set函数])。reactive({...})没有直接对应,React 对象状态也必须整体替换,这跟 Vuereactive可以car.price += 1直接改的体验完全不同。- 没有
.value这一层,直接读count,改要调setCount。
3.7. useRef(对照 ref 的两种用法)
useRef 同时对应 Vue 里的 DOM 引用 和 "不改触发渲染的变量"。
import { useRef } from 'react'
function Demo() {
// 1. 拿到 DOM 节点(对照 <h1 ref="title">)
const titleRef = useRef<HTMLHeadingElement>(null)
// 2. 存放一个不触发渲染的变量(对照普通 let 变量,但跨渲染保留)
const timerRef = useRef<number>()
function show() {
console.log(titleRef.current) // 通过 .current 访问 DOM 节点
}
return (
<div>
<h1 ref={titleRef}>标题</h1>
<button onClick={show}>打印</button>
</div>
)
}
对照 Vue3:
- 获取 DOM:Vue 用
let title = ref()+ref="title",读title.value;React 用useRef(null)+ref={titleRef},读titleRef.current。useRef的current改了不会触发重渲染,这点和 Vue 的普通变量类似,但普通变量每次渲染会被重置,useRef则跨渲染保留。
3.8. useEffect(对照 watch / watchEffect / 生命周期)
useEffect 是 React 里最"重"的 Hook,同时承担了 Vue 的 watch、watchEffect、onMounted、onUnmounted 职责。
基本结构:
useEffect(() => {
// 副作用逻辑:发请求、订阅、操作 DOM、设置定时器...
return () => {
// 可选:清理函数,组件卸载 / 依赖变化前执行(对照 onUnmounted)
}
}, [依赖数组])
情况一:不传依赖数组 → 每次渲染都执行(对照 watchEffect 的某种用法)
useEffect(() => {
console.log('组件每次渲染后都执行')
})
情况二:传空数组 [] → 只在挂载时执行一次(对照 onMounted)
import { useEffect, useState } from 'react'
function UserList() {
const [list, setList] = useState([])
useEffect(() => {
// 对照 onMounted(() => { getData() })
fetch('/api/users')
.then((res) => res.json())
.then((data) => setList(data))
}, []) // 空数组 = 只执行一次
return <ul>{list.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
}
情况三:依赖数组里写状态 → 状态变化才执行(对照 watch)
const [count, setCount] = useState(0)
useEffect(() => {
// 对照 watch(count, (newV) => {...})
console.log('count 变成了', count)
}, [count]) // 只有 count 变化才执行
情况四:带清理函数(对照 onUnmounted)
useEffect(() => {
const timer = setInterval(() => console.log('tick'), 1000)
return () => {
clearInterval(timer) // 对照 onUnmounted 里清定时器
}
}, [])
对照 Vue3 总结表:
Vue3 React onMounted(fn)useEffect(fn, [])onUnmounted(fn)useEffect(() => fn, [])的清理函数watch(x, fn)useEffect(fn, [x])watchEffect(fn)不传依赖数组,或手动罗列依赖 onUpdated无直接对应,较少用
读代码提示:
useEffect第二个参数(依赖数组)是关键,先看[]是"只跑一次",还是有具体依赖"跟着某个值跑"。
3.9. useMemo(对照 computed)
useMemo 用于"根据已有数据计算出新数据并缓存",等价于 Vue 的 computed。
import { useMemo, useState } from 'react'
function Demo() {
const [firstName, setFirstName] = useState('zhang')
const [lastName, setLastName] = useState('san')
// 对照 computed(() => firstName + '-' + lastName)
const fullName = useMemo(
() => `${firstName}-${lastName}`,
[firstName, lastName] // 依赖变化才重新计算
)
return <p>全名:{fullName}</p>
}
对照:
computed(() => ...)≈useMemo(() => ..., [依赖])。区别是 Vue 自动追踪依赖,React 要手动在第二个参数里列出依赖。
3.10. useCallback(缓存函数)
useCallback 用来"缓存一个函数",避免子组件因父组件每次渲染都新建函数而多余地重渲染。读代码时先认识它即可。
import { useCallback, useState } from 'react'
function Parent() {
const [count, setCount] = useState(0)
// 这个函数在 count 不变时,引用保持不变
const handleClick = useCallback(() => {
setCount((c) => c + 1)
}, [])
return <Child onClick={handleClick} />
}
常见于配合
React.memo(见 3.11)使用,用于性能优化。读代码时无需深究原理,知道"它是用来缓存函数的"即可。
3.11. React.memo(对照组件缓存)
React.memo 包装一个组件,让它在 props 没变时跳过重渲染。Vue 中组件默认就有类似优化,React 需要手动加。
import { memo } from 'react'
function Child({ name }: { name: string }) {
console.log('Child 渲染了')
return <p>{name}</p>
}
// 默认导出时包一层 memo
export default memo(Child)
3.12. 自定义 Hook(对照自定义 hook)
概念与 Vue3 的自定义 hook 完全一致:把复用逻辑封装成函数,函数名必须以 use 开头。这是两边最像的概念。
// hooks/useSum.ts
import { useState, useCallback } from 'react'
export function useSum(initial = 0) {
const [sum, setSum] = useState(initial)
const increment = useCallback(() => setSum((s) => s + 1), [])
const decrement = useCallback(() => setSum((s) => s - 1), [])
return { sum, increment, decrement }
}
// 组件中使用
import { useSum } from './hooks/useSum'
function App() {
const { sum, increment, decrement } = useSum()
return (
<div>
<h2>求和:{sum}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
)
}
对照:Vue 的自定义 hook 是
export default function() {...},React 是export function useXxx() {...},使用方式(在组件里解构调用)完全一样。
3.13. 组件定义与 props(对照 props / defineProps)
React 组件通过函数参数接收 props,没有 defineProps,TypeScript 用接口约束类型。
// 定义 props 类型(对照 interface + defineProps)
interface PersonProps {
name: string
age?: number // ? 表示可选
onGetToy: (value: string) => void // 函数类型 props
}
// 子组件:props 通过函数参数传入
function Child({ name, age = 18, onGetToy }: PersonProps) {
return (
<div>
<h2>姓名:{name},年龄:{age}</h2>
<button onClick={() => onGetToy('奥特曼')}>把玩具给爸爸</button>
</div>
)
}
// 父组件:像写 HTML 属性一样传 props
function Father() {
return <Child name="张三" onGetToy={(toy) => console.log(toy)} />
}
对照 Vue3:
defineProps(['name','age'])→ React 直接在函数参数里解构{ name, age }。- 默认值:Vue 用
withDefaults,React 直接在解构时写age = 18。- 子传父:Vue 用
defineEmits自定义事件;React 直接传一个回调函数作为 prop(onGetToy),子组件调用它即可。React 没有"事件"这一层抽象。
3.14. 生命周期对照总表
React 函数组件没有 onMounted 等命名钩子,全部用 useEffect 表达:
| Vue3 | React |
|---|---|
setup(创建阶段) |
组件函数体本身 |
onMounted |
useEffect(fn, []) |
onUpdated |
无直接对应(一般不写) |
onBeforeUnmount |
useEffect 返回的清理函数 |
onUnmounted |
useEffect 返回的清理函数 |
4. 路由(React Router)
React 路由最常用的是 react-router-dom,目前是 v6 版本。与 Vue Router 概念一一对应。
4.1. 安装与配置
npm install react-router-dom
src/main.tsx(React 版的路由挂载):
import { BrowserRouter } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')!).render(
// BrowserRouter = Vue 的 history 模式(createWebHistory)
<BrowserRouter>
<App />
</BrowserRouter>
)
4.2. 定义路由(对照 createRouter 的 routes)
// App.tsx
import { Routes, Route, Link, Outlet } from 'react-router-dom'
import Home from './pages/Home'
import About from './pages/About'
function App() {
return (
<div>
{/* 导航区:Link = RouterLink */}
<nav>
<Link to="/home">首页</Link>
<Link to="/about">关于</Link>
</nav>
{/* 展示区:Routes + Route = RouterView + 路由规则 */}
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
)
}
对照 Vue3 路由:
Vue Router React Router v6 <RouterLink to="/home"><Link to="/home"><RouterView /><Routes>...<Route .../></Routes>或<Outlet />{ path, component }<Route path element={<组件/>} />useRouter().push()useNavigate()useRoute().query/paramsuseParams()/useSearchParams()
4.3. 嵌套路由(对照 children)
function App() {
return (
<Routes>
<Route path="/news" element={<News />}>
{/* 子路由:path 不用写全,element 里写子组件 */}
<Route path="detail" element={<Detail />} />
</Route>
</Routes>
)
}
// News.tsx 里用 Outlet 占位(对照 <RouterView />)
function News() {
return (
<div>
<h2>新闻列表</h2>
<Outlet /> {/* 子路由组件渲染到这里 */}
</div>
)
}
4.4. 路由传参
params(路径参数,对照 Vue 的 params)
// 定义时用 :id 占位
<Route path="/user/:id" element={<User />} />
// 跳转
<Link to="/user/123">用户 123</Link>
// 接收:useParams
import { useParams } from 'react-router-dom'
function User() {
const { id } = useParams() // 对照 route.params.id
return <h1>用户 ID:{id}</h1>
}
query(搜索参数,对照 Vue 的 query)
// 跳转
<Link to="/search?keyword=react&page=1">搜索</Link>
// 接收:useSearchParams(比 Vue 麻烦一点)
import { useSearchParams } from 'react-router-dom'
function Search() {
const [searchParams] = useSearchParams()
const keyword = searchParams.get('keyword') // 对照 route.query.keyword
return <h1>关键词:{keyword}</h1>
}
4.5. 编程式导航(对照 useRouter)
import { useNavigate } from 'react-router-dom'
function Demo() {
const navigate = useNavigate()
function goHome() {
navigate('/home') // 对照 router.push('/home')
}
function goBack() {
navigate(-1) // 对照 router.back()
}
function replaceTo() {
navigate('/login', { replace: true }) // 对照 router.replace()
}
function goWithParams() {
navigate('/user/123') // params 直接拼在路径里
}
function goWithQuery() {
navigate('/search?keyword=react') // query 拼在路径后
}
return <button onClick={goHome}>去首页</button>
}
4.6. 两种模式(对照 history / hash)
| 模式 | Vue3 | React |
|---|---|---|
| history(URL 无 #) | createWebHistory() |
<BrowserRouter> |
| hash(URL 带 #) | createWebHashHistory() |
<HashRouter> |
5. 状态管理(对照 Pinia)
React 状态管理方案很多,已有项目中最常见的是 Redux Toolkit(RTK),其次是 Context + useReducer、Zustand。下面重点讲最可能遇到的 RTK,再简单介绍 Context。
5.1. Redux Toolkit(对照 Pinia)
核心概念对照
| Pinia | Redux Toolkit |
|---|---|
state |
state(必须整体替换) |
getters |
createSelector / 组件里用 useMemo |
actions |
reducers 里的函数 |
| 修改数据 | dispatch(action) |
storeToRefs |
useSelector 读数据 |
搭建与使用
npm install @reduxjs/toolkit react-redux
第一步:创建 store(对照 store/count.ts)
// store/counterSlice.ts —— 一个"切片",对应一个 Pinia 的 store
import { createSlice } from '@reduxjs/toolkit'
const counterSlice = createSlice({
name: 'counter', // 对照 defineStore('count', {...})
initialState: { // 对照 state()
sum: 0,
},
reducers: { // 对照 actions
increment(state, action) {
state.sum += action.payload // 注意:RTK 内部用 immer,允许"看似原地修改"
},
decrement(state, action) {
state.sum -= action.payload
},
},
})
export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer
第二步:汇总成 store(对照 createPinia)
// store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer, // 多个 slice 都挂在这里
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
第三步:注入 Provider(对照 app.use(pinia))
// main.tsx
import { Provider } from 'react-redux'
import { store } from './store'
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<App />
</Provider>
)
第四步:组件中使用(对照 useXxxStore)
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/counterSlice'
import type { RootState } from './store'
function Counter() {
// 读数据:useSelector,对照 useSumStore().sum
const sum = useSelector((state: RootState) => state.counter.sum)
// 修改数据:dispatch(action)
const dispatch = useDispatch()
return (
<div>
<h2>求和:{sum}</h2>
<button onClick={() => dispatch(increment(1))}>+1</button>
<button onClick={() => dispatch(decrement(1))}>-1</button>
</div>
)
}
对照 Pinia:
useSumStore().sum≈useSelector((s) => s.counter.sum)countStore.increment(n)≈dispatch(increment(n))- Pinia 的
action里可以写业务逻辑,RTK 里业务逻辑通常写在createAsyncThunk或组件里。
5.2. Context(对照 provide / inject)
Context 是 React 内置的跨层级通信方案,对应 Vue 的 provide / inject,也常被用作轻量状态管理。
import { createContext, useContext, useState } from 'react'
// 1. 创建 Context(对照 provide 的 key)
const ThemeContext = createContext('light')
function App() {
const [theme, setTheme] = useState('light')
return (
// 2. Provider 提供数据(对照 provide('theme', value))
<ThemeContext.Provider value={theme}>
<Child />
</ThemeContext.Provider>
)
}
function Child() {
return <GrandChild />
}
function GrandChild() {
// 3. useContext 消费数据(对照 inject('theme'))
const theme = useContext(ThemeContext)
return <p>当前主题:{theme}</p>
}
对照:
provide/inject≈Context.Provider+useContext。区别是 React 的 Context 值变化会触发所有消费它的组件重渲染,所以常配合useMemo缓存。
6. 组件通信对照总表
| 场景 | Vue3 | React |
|---|---|---|
| 父传子 | props |
函数参数 props |
| 子传父 | defineEmits + emit('事件', 数据) |
传回调函数 props.onXxx(数据) |
| 祖孙通信 | provide / inject |
Context.Provider + useContext |
| 任意组件通信 | mitt / pinia |
状态管理库(Redux/Zustand)或 Context |
| 插槽 | <slot> / 具名插槽 / 作用域插槽 |
props.children / 具名 props / render props |
| DOM/组件引用 | ref + defineExpose |
useRef(React 无 defineExpose,默认拿不到子组件实例) |
6.1. 插槽(对照 slot)
React 没有 slot 指令,默认插槽就是 children,具名插槽就是普通 prop,作用域插槽就是 render props。
// 默认插槽(对照 <slot></slot>)
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>
}
// 使用
<Card>
<ul><li>内容</li></ul>
</Card>
// 具名插槽(对照 <slot name="header">)
function Layout({ header, footer, children }: { header: React.ReactNode; footer: React.ReactNode; children: React.ReactNode }) {
return (
<div>
<header>{header}</header>
<main>{children}</main>
<footer>{footer}</footer>
</div>
)
}
// 使用
<Layout
header={<h1>页头</h1>}
footer={<p>页脚</p>}
>
<p>主体内容</p>
</Layout>
// 作用域插槽(对照 v-slot="params"):用 render props 实现
function GameList({ render }: { render: (games: Game[]) => React.ReactNode }) {
const games = [{ id: 'a01', name: '英雄联盟' }]
return <div>{render(games)}</div>
}
// 使用:传一个函数,函数参数就是子组件暴露的数据
<GameList render={(games) => (
<ul>{games.map((g) => <li key={g.id}>{g.name}</li>)}</ul>
)} />
对照:
<slot>→children;<slot name="x">→ 具名 prop;v-slot="params"→ render props(传函数)。
7. 其它常见 API
7.1. useReducer(对照复杂状态管理)
当状态逻辑复杂(多个子状态相互关联)时,用 useReducer,它类似 Redux 的迷你版。
import { useReducer } from 'react'
interface State { count: number }
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: 0 }
}
}
function Demo() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div>
<h2>计数:{state.count}</h2>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
</div>
)
}
7.2. Fragment(对照模板多根节点)
Vue3 模板可以直接写多个根标签,React 的 JSX 必须有一个根元素,用 <></>(Fragment)占位即可,不会渲染出真实 DOM。
function Demo() {
return (
<>
<h1>标题1</h1>
<h2>标题2</h2>
</>
)
}
// 等价于 <React.Fragment>...</React.Fragment>
7.3. createPortal(对照 Teleport)
import { createPortal } from 'react-dom'
function Modal({ isShow }: { isShow: boolean }) {
if (!isShow) return null
// 把弹窗 DOM 渲染到 body 下(对照 <Teleport to="body">)
return createPortal(
<div className="modal">
<h2>我是一个弹窗</h2>
</div>,
document.body
)
}
对照:
<Teleport to="body">≈createPortal(jsx, document.body)。
7.4. lazy + Suspense(对照 defineAsyncComponent + Suspense)
import { lazy, Suspense } from 'react'
// 异步引入组件(对照 defineAsyncComponent(() => import(...)))
const Child = lazy(() => import('./Child'))
function App() {
return (
// Suspense 的 fallback 对照 Vue 的 v-slot:fallback
<Suspense fallback={<h3>加载中......</h3>}>
<Child />
</Suspense>
)
}
对照:Vue 的
<Suspense>需要<template #default>/<template #fallback>;React 直接用fallback属性 + 子组件就是 default。
7.5. Class 组件(老项目遗留代码)
老 React 项目会用 Class 组件,读代码时认识即可,现代写法已全面转向函数组件 + Hooks。
import { Component } from 'react'
interface Props { name: string }
interface State { count: number }
class Counter extends Component<Props, State> {
state: State = { count: 0 } // 对照 data()
handleClick = () => { // 对照 methods
this.setState({ count: this.state.count + 1 }) // 对照修改 data
}
componentDidMount() { // 对照 onMounted
console.log('挂载了')
}
componentWillUnmount() { // 对照 onUnmounted
console.log('卸载了')
}
render() { // 对照 <template>
return (
<div>
<h1>{this.props.name}</h1> {/* 对照 this.xxx 访问 props */}
<h2>计数:{this.state.count}</h2>
<button onClick={this.handleClick}>+1</button>
</div>
)
}
}
Class 组件生命周期对照:
componentDidMount= onMounted;componentDidUpdate= onUpdated;componentWillUnmount= onUnmounted。
8. 读 React 代码的速查清单
读一个陌生 React 文件时,按以下顺序快速理解:
- 看组件签名:
function Xxx(props)是函数组件,class Xxx extends Component是类组件。 - 看 import:
useState(状态)、useEffect(副作用/请求)、useRef(DOM/持久变量)、useMemo(计算属性)、useCallback(缓存函数)、useContext(跨层数据)。 - 看 useState 返回的解构:
const [xxx, setXxx] = useState(...),xxx是数据,setXxx是唯一修改入口。 - 看 useEffect 第二参数:
[]只跑一次,[a, b]跟着 a/b 跑,不写则每次渲染都跑。 - 看 JSX 里的
{ }:花括号里都是 JS 表达式,{list.map(...)}是循环,{cond && ...}或三元是条件。 - 看数据流向:数据永远是"从上往下"通过 props 传,子改父靠回调函数,跨层靠 Context 或状态库。
- 遇到不认识的
useXxx:大概率是自定义 Hook 或第三方 Hook(如useNavigate、useSelector),按名字猜用途即可。
8.1. 一张对照速查总表
| 需求 | Vue3 | React |
|---|---|---|
| 定义状态 | ref() / reactive() |
useState() |
| 读状态 | xxx.value / xxx |
xxx |
| 改状态 | xxx.value = 1 / obj.a = 1 |
setXxx(新值),对象整体替换 |
| 计算属性 | computed() |
useMemo() |
| 监听 | watch / watchEffect |
useEffect |
| 挂载钩子 | onMounted |
useEffect(fn, []) |
| 卸载钩子 | onUnmounted |
useEffect 清理函数 |
| DOM 引用 | ref + ref="xx" |
useRef + ref={xx},读 .current |
| 循环 | v-for + :key |
.map() + key |
| 条件 | v-if / v-show |
&& / 三元 / 控制 display |
| 双向绑定 | v-model |
value + onChange(受控组件) |
| 事件 | @click |
onClick |
| 插槽 | <slot> |
children / 具名 prop |
| 父子通信 | props + emit | props + 回调函数 |
| 祖孙通信 | provide/inject | Context |
| 状态管理 | Pinia | Redux Toolkit / Zustand |
| 路由 | vue-router | react-router-dom v6 |
| 传送 DOM | Teleport | createPortal |
| 异步组件 | defineAsyncComponent | lazy + Suspense |
| 多根节点 | 原生支持 | <></> Fragment |
| 自定义复用逻辑 | 自定义 hook | 自定义 Hook(useXxx) |
结语:React 相比 Vue3,少了"响应式魔法"(ref/reactive/computed/watch 的自动依赖追踪),换来的是"一切皆函数、一切皆显式"的简单心智模型。读代码时抓住两条主线即可:① 状态在哪定义、怎么改(useState);② 状态变化后组件会整体重新执行(JSX 重新计算)。其余 API 都是围绕这两点的补充。