目的:用你已经熟悉的 Vue3 心智模型(响应式数据 / 计算属性 / watch / watchEffect)作锚点,建立 React 中对应概念的映射。React 没有「响应式系统」这一统一内核,而是把「状态」和「副作用」拆成显式 API,理解这个差异是关键。
📋 一图速览(核心映射表)
| Vue3 概念 | React 函数组件(Hooks) | React 类组件 |
|---|---|---|
ref / reactive(响应式数据) |
useState / useReducer / useRef |
this.state + this.setState |
computed(计算属性) |
useMemo(派生值) |
类字段 getter / render 中直接计算 / PureComponent |
watch(侦听特定源) |
useEffect(带依赖数组) / useLayoutEffect |
componentDidUpdate 中比对 prevProps/prevState |
watchEffect(自动追踪依赖) |
useEffect(无依赖数组 = 每次渲染都跑) |
暂无直接对应,靠手动在生命周期里组织 |
watch 的 immediate |
useEffect 天然「挂载即执行」 |
componentDidMount |
watch 的 deep |
靠依赖数组里引用具体字段 / useDeepCompareEffect |
手动深比较 |
readonly / shallowRef |
useMemo 缓存 / React.memo / useRef(可变但不触发渲染) |
shouldComponentUpdate 控制 |
effectScope / onScopeDispose |
自定义 Hook 返回清理函数 + useEffect 清理 |
生命周期方法组合 |
🔰 第一部分:Vue3 概念回顾(作为对照基准)
1. 响应式数据:ref / reactive
- Vue3 用 Proxy 把数据变成「响应式对象」,读取即收集依赖,修改即触发更新。
- 组件自动「订阅」用到的响应式数据,无需手动声明依赖。
2. 计算属性:computed
- 基于其他响应式数据派生出来的值,带缓存(依赖不变则不重算)。
3. 侦听:watch 与 watchEffect
watch:显式 指定要监听的源(一个或多个),源变化才执行回调,支持immediate/deep/ 新旧值。watchEffect:隐式自动收集回调里用到的所有响应式依赖,立即执行一次并持续追踪。
🔰 第二部分:React 函数组件(Hooks)的对应实现
React 的核心理念:UI 是状态的纯函数 (
UI = f(state))。状态变了就重新执行整个函数(重新渲染),而不是「精准更新某个字段」。
1. 响应式数据 → useState / useReducer / useRef
jsx
import { useState, useReducer, useRef } from 'react'
// ✅ 等价于 Vue 的 ref(基础类型 / 任意值)
function Counter() {
const [count, setCount] = useState(0) // count 即响应式数据,setCount 即修改入口
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
// ✅ 等价于 Vue 的 reactive(复杂对象 / 多字段联动)
function useForm() {
const [form, setForm] = useReducer(
(state, action) => ({ ...state, [action.field]: action.value }),
{ name: '', age: 0 }
)
// setForm({ field: 'name', value: 'lmw' })
return form
}
// ✅ useRef:可变值,但修改不会触发重新渲染(≈ Vue 的 shallowRef 不动 UI,或纯粹的实例变量)
function Timer() {
const timerRef = useRef(null)
useEffect(() => {
timerRef.current = setInterval(() => console.log('tick'), 1000)
return () => clearInterval(timerRef.current)
}, [])
return null
}
关键差异
- Vue 里
count.value++直接改;React 里必须调用setCount(不可直接改 state 本身,它是不可变数据)。 - React 的「响应式」本质是:调用 setState → 组件函数重跑 → 返回新 JSX → 框架 diff 后更新 DOM。不是 Proxy 代理。
2. 计算属性 → useMemo
jsx
import { useMemo } from 'react'
function Order({ items, discount }) {
// ✅ 等价于 Vue 的 computed:依赖 (items, discount) 不变则不重算
const total = useMemo(() => {
const sum = items.reduce((s, i) => s + i.price * i.qty, 0)
return sum * (1 - discount)
}, [items, discount])
return <div>合计:{total}</div>
}
Vue computed |
React useMemo |
|---|---|
| 默认带缓存 | 默认带缓存(依赖数组) |
| 可写(get/set) | 只做派生,不能直接「赋值」回去(如需回写请用 useState + useEffect) |
| 在模板里像普通属性用 | 在 JSX / 逻辑里像普通变量用 |
⚠️ 注意:useMemo 主要为了性能优化(避免每次渲染重复计算),并非语义上的「响应式计算」。React 官方强调不要把它当成 Vue 的 computed 来依赖其「保证执行时机」。
3. 侦听 watch → useEffect(带依赖数组)
jsx
import { useEffect } from 'react'
function Profile({ userId }) {
const [user, setUser] = useState(null)
// ✅ 等价于 Vue 的 watch(userId, ...):userId 变化才执行
useEffect(() => {
let cancelled = false
fetch(`/api/user/${userId}`)
.then(r => r.json())
.then(data => { if (!cancelled) setUser(data) })
return () => { cancelled = true } // ✅ 清理函数 ≈ Vue watch 的清理 / onScopeDispose
}, [userId]) // ← 依赖数组就是「监听源」
return <div>{user ? user.name : '加载中'}</div>
}
[userId]就是「我要监听 userId」的显式声明------和 Vuewatch显式指定源一致。useEffect的清理函数(return)对应 Vue 的停止侦听 / 资源释放。- 没有
immediate选项?useEffect在挂载时本来就执行一次 ,天然等于watch(..., { immediate: true })。
4. 侦听 watchEffect → useEffect(无依赖数组)
jsx
// ✅ 等价于 Vue 的 watchEffect:回调里用到什么就「追踪」什么,且立即执行
function Logger({ a, b }) {
useEffect(() => {
console.log(`a=${a}, b=${b}`) // 用到 a、b → 它们变了就重跑
}) // ← 不传依赖数组 = 每次渲染都执行(≈ watchEffect 的自动追踪,但粒度是「整个渲染」)
return null
}
⚠️ 严格说 React 没有「自动追踪某个变量」的机制。不传依赖数组 = 每次渲染都跑;传空数组
[]= 只挂载跑一次(既不追踪也不重跑)。最接近watchEffect心智 的是「不传依赖数组」,但代价是每次渲染都执行,所以实战中几乎总是显式传依赖数组 (回到watch模式)。
5. useLayoutEffect:同步副作用(贴近 DOM 的 watch)
jsx
import { useLayoutEffect, useRef } from 'react'
function Measured() {
const ref = useRef(null)
useLayoutEffect(() => {
// DOM 更新后、浏览器绘制前同步执行,适合读取布局/测量
console.log(ref.current.getBoundingClientRect())
}, [])
return <div ref={ref}>测量我</div>
}
- 等价于「需要在 DOM 变更后、绘制前同步做点事」的 watch(Vue 里没有严格对应的同步钩子,nextTick 反而更接近微任务)。
6. 组合复用 → 自定义 Hook(≈ Composition API / setup)
jsx
// ✅ 自定义 Hook 就是 Vue composable 的等价物
function useUser(userId) {
const [user, setUser] = useState(null)
useEffect(() => { /* fetch */ }, [userId])
return user
}
Vue 的 setup() 里 return { ... } 导出给模板用;React 的自定义 Hook 直接 return 给调用方用。
🔰 第三部分:React 类组件的对应实现
类组件没有 Hooks,状态与生命周期都挂在组件实例上。它是 React 早期的写法,现在新项目基本用函数组件,但理解它有助于看懂老代码、也更能体会 Hooks 解决了什么痛点(逻辑难复用、this 绑定混乱)。
1. 响应式数据 → this.state + this.setState
jsx
import React, { Component } from 'react'
class Counter extends Component {
constructor(props) {
super(props)
this.state = { count: 0 } // ✅ 等价于 Vue 的 reactive({ count: 0 })
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState(prev => ({ count: prev.count + 1 })) // ✅ 修改入口
}
render() {
return <button onClick={this.handleClick}>{this.state.count}</button>
}
}
this.state是响应式数据,this.setState是修改入口(异步合并更新)。- 等价于 Vue:
data()返回的对象 + 直接改即响应式。
2. 计算属性 → render 中直接计算 / getter
jsx
class Order extends Component {
state = { items: [], discount: 0.1 }
// ✅ 用 getter 模拟 computed(但每次 render 都重算,无缓存)
get total() {
const sum = this.state.items.reduce((s, i) => s + i.price * i.qty, 0)
return sum * (1 - this.state.discount)
}
render() {
return <div>合计:{this.total}</div>
}
}
- 想要缓存?可把派生值存进
state并在componentDidUpdate里更新(麻烦,这正是 Hooks 用useMemo简化的地方)。 - 想要「像 computed 一样自动跟着变」:类组件没有内建,纯靠
render每次重算。
3. 侦听 watch → componentDidUpdate
jsx
class Profile extends Component {
state = { user: null }
// ✅ 等价于 watch(userId):比对前后 props
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId)
}
}
componentDidMount() {
this.fetchUser(this.props.userId) // ✅ 等价于 watch immediate
}
fetchUser(userId) {
fetch(`/api/user/${userId}`).then(r => r.json()).then(user => this.setState({ user }))
}
render() { /* ... */ }
}
componentDidUpdate(prevProps, prevState)里手动比对新旧值 = Vuewatch的(newVal, oldVal)。componentDidMount=immediate: true。- 清理/取消请求:在
componentWillUnmount里做(≈onScopeDispose)。
4. 派生状态 / 控制重渲染
jsx
class List extends Component {
// ✅ 等价于 Vue 的 shallowRef / 只读:返回 false 阻止无谓更新
shouldComponentUpdate(nextProps, nextState) {
return nextProps.items !== this.props.items
}
render() { /* ... */ }
}
// ✅ 等价于 React.memo(函数组件):自动浅比较
class PureList extends React.PureComponent {
render() { /* 自动对 props/state 浅比较 */ }
}
🔰 第四部分:心智模型的根本差异(最重要)
Vue:响应式系统(自动追踪)
javascript
数据变了 ──(Proxy 自动通知)──> 用到它的地方自动更新
- 你声明 依赖关系(模板里用了谁、computed/watch 里用了谁),框架自动追踪。
- 改数据即可,UI 自己跟着变。
React:状态不可变 + 显式触发重渲染
setState ──> 组件函数重跑 ──> 生成新虚拟 DOM ──> diff ──> 更新真实 DOM
- React 不知道你改了「哪个字段」,它只知道「你调用了 setState」。
- 然后整个组件函数重新执行,你用到的 state 自然是最新的。
- 副作用(请求、订阅、DOM 操作)必须显式 放进
useEffect并声明依赖数组,否则要么不执行、要么无限循环。
💡 一句话记忆:Vue 是「数据驱动、自动订阅」;React 是「状态不可变、整体重渲染、副作用显式声明」。
🔰 第五部分:常见坑对照
| 场景 | Vue3 | React 函数组件 |
|---|---|---|
| 修改数组/对象 | reactive 直接改,ref 改 .value |
必须返回新引用 (如 setList([...list, x])),直接 list.push 不生效 |
| 派生值缓存 | computed 自动 |
useMemo(仅优化,非语义保证) |
| 监听多个源 | watch([a, b], ...) |
useEffect(fn, [a, b]) |
| 立即执行 | watch(..., { immediate: true }) |
useEffect 默认挂载即执行 |
| 卸载清理 | onScopeDispose / watch 返回 stop |
useEffect 的 return 清理函数 / componentWillUnmount |
| 拿到旧值 | watch 回调参数 (newV, oldV) |
useEffect 闭包捕获的是「上一次渲染的值」;或 useRef 手动存 |
🎯 总结练习(给自己出的对照题)
- Vue 的
const double = computed(() => count.value * 2)→ React 函数组件怎么写?(const double = useMemo(() => count * 2, [count])) - Vue 的
watch(count, v => localStorage.setItem('c', v))→ React 类组件怎么写?(componentDidUpdate(prev){ if(prev.count!==this.state.count) localStorage.setItem('c', this.state.count) }) - Vue 的
watchEffect(() => console.log(count.value))→ React 函数组件怎么写?(useEffect(() => { console.log(count) })不传依赖数组;或更好的是useEffect(() => {...}, [count]))
关联笔记:Vue 3 响应式三剑客:ref、shallowRef、reactive;Vue3 watch与watchEffect详解;React16与React18学习笔记