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),只更新变化的部分。

对照 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 对象状态也必须整体替换,这跟 Vue reactive 可以 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
  • useRefcurrent 改了不会触发重渲染,这点和 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 直接传一个回调函数作为 proponGetToy),子组件调用它即可。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/params useParams() / 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().sumuseSelector((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/injectContext.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 文件时,按以下顺序快速理解:

  1. 看组件签名function Xxx(props) 是函数组件,class Xxx extends Component 是类组件。
  2. 看 importuseState(状态)、useEffect(副作用/请求)、useRef(DOM/持久变量)、useMemo(计算属性)、useCallback(缓存函数)、useContext(跨层数据)。
  3. 看 useState 返回的解构const [xxx, setXxx] = useState(...)xxx 是数据,setXxx 是唯一修改入口。
  4. 看 useEffect 第二参数[] 只跑一次,[a, b] 跟着 a/b 跑,不写则每次渲染都跑。
  5. 看 JSX 里的 { }:花括号里都是 JS 表达式,{list.map(...)} 是循环,{cond && ...} 或三元是条件。
  6. 看数据流向:数据永远是"从上往下"通过 props 传,子改父靠回调函数,跨层靠 Context 或状态库。
  7. 遇到不认识的 useXxx:大概率是自定义 Hook 或第三方 Hook(如 useNavigateuseSelector),按名字猜用途即可。

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 都是围绕这两点的补充。