Vue3 响应式概念 ↔ React 对应方案(类组件与函数组件)对照笔记

目的:用你已经熟悉的 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」的显式声明------和 Vue watch 显式指定源一致。
  • 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) 里手动比对新旧值 = Vue watch 的 (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 手动存

🎯 总结练习(给自己出的对照题)

  1. Vue 的 const double = computed(() => count.value * 2) → React 函数组件怎么写?(const double = useMemo(() => count * 2, [count]))
  2. Vue 的 watch(count, v => localStorage.setItem('c', v)) → React 类组件怎么写?(componentDidUpdate(prev){ if(prev.count!==this.state.count) localStorage.setItem('c', this.state.count) })
  3. Vue 的 watchEffect(() => console.log(count.value)) → React 函数组件怎么写?(useEffect(() => { console.log(count) }) 不传依赖数组;或更好的是 useEffect(() => {...}, [count]))

关联笔记:Vue 3 响应式三剑客:ref、shallowRef、reactive;Vue3 watch与watchEffect详解;React16与React18学习笔记

相关推荐
卸任4 小时前
Vibe coding工具使用心得
前端·ai编程
brien94 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance5 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M5 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写5 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李5 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易5 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马5 小时前
前沿视界
前端
kyriewen5 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易6 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员