📋 概念速查表

Vue 2 到 React 的核心概念一一映射

Vue 2 概念 React 对应 说明
data() 返回响应式对象 useState Hook 状态声明方式不同,Vue 是对象,React 是独立变量
computed useMemo 计算/派生状态,依赖变化时重新计算
watch useEffect 副作用监听,但 React 的 useEffect 语义更广
methods 普通函数 / useCallback Vue 的 methods 挂在实例上,React 直接定义在函数体内
mounted useEffect([], ...) 组件挂载后执行,依赖数组传空数组
updated useEffect 无依赖 每次渲染后执行,需注意避免死循环
beforeDestroy useEffect 返回函数 清理函数在组件卸载前执行
v-model 受控组件 + onChange Vue 双向绑定,React 需手动同步状态
v-if / v-show 条件渲染(&&& / 三元) v-if 对应条件渲染,v-show 对应 CSS display
v-for array.map() 列表渲染,React 需要显式指定 key
ref useRef DOM 引用,React 的 useRef 也可存储任意可变值
provide/inject Context 跨层级通信,避免 props 逐层传递
💡
迁移提示

这张表是你的核心参考。迁移时不必一次理解全部差异,建议按模块逐步对照,先从 datauseState 开始建立直觉。

响应式原理对比

理解两种框架的状态追踪机制差异

Vue 2 Object.defineProperty

Vue 2 使用 Object.defineProperty 递归劫持 data 对象的每个属性,在 getter 中收集依赖,在 setter 中通知更新。

data: { count: 0 } getter 劫持 收集依赖
this.count = 1 setter 触发 派发更新

无法检测新增属性(需 Vue.set)和数组索引修改。

React 不可变状态 + 重新渲染

React 不做数据劫持。调用 setState 时创建新引用,触发组件重新渲染,由开发者决定何时以及如何更新。

useState(0) setCount(1) re-render
setState(prev => ...) 浅比较新旧 调度更新

对象/数组必须创建新引用,否则 React 认为没有变化。

// Vue 2 - 响应式数据自动追踪
export default {
  data() {
    return {
      count: 0,
      user: { name: 'Alice', age: 25 }
    }
  },
  computed: {
    // 自动追踪 this.count 的变化
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++  // 自动触发视图更新
    },
    addProp() {
      // 无法检测!需要用 Vue.set
      this.$set(this.user, 'email', 'a@b.com')
    }
  }
}
import { useState, useMemo } from 'react'

interface User {
  name: string
  age: number
  email?: string
}

function Counter() {
  const [count, setCount] = useState<number>(0)
  const [user, setUser] = useState<User>({
    name: 'Alice', age: 25
  })

  // 手动追踪 count 的派生值
  const doubleCount = useMemo(
    () => count * 2,
    [count]
  )

  const increment = () => {
    setCount(prev => prev + 1)
  }

  const addProp = () => {
    // 创建新对象即可,无需特殊 API
    setUser(prev => ({ ...prev, email: 'a@b.com' }))
  }
}

🔄 生命周期映射

从 Options API 钩子到 useEffect 的思维转换

beforeCreate / created 函数体顶部

Vue 2 的这两个钩子对应 React 函数组件的"函数体本身"——组件函数执行即代表初始化。

mounted useEffect(() => { ... }, [])

组件挂载到 DOM 后执行。依赖数组传空数组,确保只在首次渲染后运行一次。

updated useEffect(() => { ... })

省略依赖数组时,每次渲染后都会执行。注意不要在其中修改状态导致死循环。

watch: { prop } useEffect(() => { ... }, [prop])

指定依赖数组可以精确监听特定值的变化,类似 Vue 的 watch 选项。

beforeDestroy useEffect 的返回函数

清理副作用。useEffect 返回的函数在组件卸载前和下次 effect 执行前调用。

errorCaptured ErrorBoundary (class component)

错误边界目前只能用 class 组件实现,用于捕获子组件的渲染错误。

export default {
  data() {
    return { items: [], timer: null }
  },

  // 组件挂载后
  mounted() {
    this.fetchItems()
    this.timer = setInterval(() => {
      this.fetchItems()
    }, 5000)
  },

  // 监听特定数据变化
  watch: {
    items(newVal, oldVal) {
      console.log('items changed', newVal.length)
    }
  },

  // 组件销毁前清理
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer)
    }
  }
}
import { useState, useEffect } from 'react'

interface Item {
  id: string
  name: string
}

function ItemList() {
  const [items, setItems] = useState<Item[]>([])

  // mounted + beforeDestroy 合并到一个 effect
  useEffect(() => {
    fetchItems()

    const timer = setInterval(() => {
      fetchItems()
    }, 5000)

    // cleanup: 等价于 beforeDestroy
    return () => {
      clearInterval(timer)
    }
  }, [])  // 空依赖 = 只在 mount/unmount 时执行

  // watch items 的变化
  useEffect(() => {
    console.log('items changed', items.length)
  }, [items])  // 依赖数组 = watch 目标
}
关键区别

Vue 的 watch 可以拿到旧值和新值,而 useEffect 只能拿到最新值。 如果需要旧值,需要用 useRef 手动保存。

📝 模板语法对比

从 Vue 模板指令到 JSX 表达式的转换

🔀
条件渲染
v-if / v-else → && / 三元运算符
<!-- v-if 完全移除 DOM -->
<div v-if="isLoggedIn">
  <p>欢迎回来</p>
</div>
<div v-else>
  <p>请登录</p>
</div>

<!-- v-show 仅切换 display -->
<div v-show="isVisible">
  可见内容
</div>
// && 短路 —— 适合"有或无"
{isLoggedIn && <p>欢迎回来</p>}

// 三元 —— 适合"二选一"
{isLoggedIn
  ? <p>欢迎回来</p>
  : <p>请登录</p>
}

// v-show 等价:CSS 控制
<div style={{ display: isVisible ? 'block' : 'none' }}>
  可见内容
</div>
🔁
列表渲染
v-for → Array.map()
<!-- 基础列表 -->
<ul>
  <li
    v-for="(item, index) in items"
    :key="item.id"
  >
    {{ index + 1 }}. {{ item.name }}
  </li>
</ul>

<!-- 遍历对象 -->
<div
  v-for="(value, key) in user"
  :key="key"
>
  {{ key }}: {{ value }}
</div>
// 基础列表
<ul>
  {items.map((item, index) => (
    <li key={item.id}>
      {index + 1}. {item.name}
    </li>
  ))}
</ul>

// 遍历对象
{Object.entries(user).map(
  ([key, value]) => (
    <div key={key}>
      {key}: {value}
    </div>
  )
)}
🔗
双向绑定
v-model → 受控组件 + onChange
<template>
  <form @submit.prevent="onSubmit">
    <input
      v-model="form.name"
      placeholder="姓名"
    />
    <select v-model="form.role">
      <option value="admin">管理员</option>
      <option value="user">用户</option>
    </select>
    <input
      type="checkbox"
      v-model="form.agree"
    />
  </form>
</template>
const [form, setForm] = useState({
  name: '',
  role: 'admin',
  agree: false,
})

const handleChange = (
  e: React.ChangeEvent<
    HTMLInputElement | HTMLSelectElement
  >
) => {
  const { name, value, type } = e.target
  setForm(prev => ({
    ...prev,
    [name]: type === 'checkbox'
      ? (e.target as HTMLInputElement).checked
      : value
  }))
}

<form onSubmit={onSubmit}>
  <input
    name="name"
    value={form.name}
    onChange={handleChange}
  />
  <select
    name="role"
    value={form.role}
    onChange={handleChange}
  >
    <option value="admin">管理员</option>
    <option value="user">用户</option>
  </select>
  <input
    type="checkbox"
    name="agree"
    checked={form.agree}
    onChange={handleChange}
  />
</form>
🧮
计算属性
computed → useMemo
export default {
  data() {
    return {
      price: 100,
      quantity: 3,
      discount: 0.8
    }
  },
  computed: {
    // 自动缓存,依赖不变则不重算
    total() {
      return this.price
        * this.quantity
        * this.discount
    },
    formattedTotal() {
      return `¥${this.total.toFixed(2)}`
    }
  }
}
function Cart() {
  const [price] = useState(100)
  const [quantity] = useState(3)
  const [discount] = useState(0.8)

  // 依赖不变时返回缓存值
  const total = useMemo(
    () => price * quantity * discount,
    [price, quantity, discount]
  )

  // 链式派生
  const formattedTotal = useMemo(
    () => `¥${total.toFixed(2)}`,
    [total]
  )
}

📦 完整组件对比

一个真实场景:带搜索、过滤、计数器的用户列表

<template>
  <div class="user-list">
    <h2>用户列表 ({{ filteredCount }})</h2>

    <!-- 搜索框:v-model 双向绑定 -->
    <input
      v-model="search"
      placeholder="搜索用户..."
    />

    <!-- 条件渲染:加载状态 -->
    <div v-if="loading">加载中...</div>

    <!-- 列表渲染 -->
    <ul v-else>
      <li
        v-for="user in filteredUsers"
        :key="user.id"
        @click="selectUser(user)"
      >
        {{ user.name }}
      </li>
    </ul>

    <!-- 选中用户详情 -->
    <div v-if="selectedUser">
      <p>选中: {{ selectedUser.name }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: [],
      search: '',
      loading: false,
      selectedUser: null,
    }
  },
  computed: {
    filteredUsers() {
      return this.users.filter(u =>
        u.name.toLowerCase()
          .includes(this.search.toLowerCase())
      )
    },
    filteredCount() {
      return this.filteredUsers.length
    }
  },
  mounted() {
    this.fetchUsers()
  },
  methods: {
    async fetchUsers() {
      this.loading = true
      const res = await fetch('/api/users')
      this.users = await res.json()
      this.loading = false
    },
    selectUser(user) {
      this.selectedUser = user
    }
  }
}
</script>
import React, {
  useState, useEffect, useMemo, useCallback
} from 'react'

interface User {
  id: string
  name: string
}

const UserList: React.FC = () => {
  const [users, setUsers] = useState<User[]>([])
  const [search, setSearch] = useState('')
  const [loading, setLoading] = useState(false)
  const [selectedUser, setSelectedUser] =
    useState<User | null>(null)

  // computed: filteredUsers
  const filteredUsers = useMemo(() => {
    return users.filter(u =>
      u.name.toLowerCase()
        .includes(search.toLowerCase())
    )
  }, [users, search])

  // computed: filteredCount(直接用派生值)
  const filteredCount = filteredUsers.length

  // mounted: 获取数据
  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true)
      const res = await fetch('/api/users')
      const data = await res.json()
      setUsers(data)
      setLoading(false)
    }
    fetchUsers()
  }, [])

  // methods: 用 useCallback 缓存
  const selectUser = useCallback(
    (user: User) => setSelectedUser(user),
    []
  )

  return (
    <div className="user-list">
      <h2>用户列表 ({filteredCount})</h2>

      <input
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder="搜索用户..."
      />

      {loading ? (
        <div>加载中...</div>
      ) : (
        <ul>
          {filteredUsers.map(user => (
            <li
              key={user.id}
              onClick={() => selectUser(user)}
            >
              {user.name}
            </li>
          ))}
        </ul>
      )}

      {selectedUser && (
        <div>
          <p>选中: {selectedUser.name}</p>
        </div>
      )}
    </div>
  )
}

export default UserList
💡
迁移要点

注意对比中的模式:Vue 的 computed 自动缓存 vs React 的 useMemo 需手动声明依赖; Vue 的 methods 不需要缓存 vs React 的函数每次渲染都重新创建(可用 useCallback 优化)。

🪝 React Hooks 速查

迁移时最常用的 Hooks 及其 Vue 对应关系

useState data() useEffect mounted / watch / beforeDestroy useMemo computed useCallback methods (缓存版) useRef ref / this.$refs useContext provide / inject useReducer Vuex (局部) useLayoutEffect updated (同步)
<template>
  <input ref="myInput" />
  <button @click="focusInput">
    聚焦
  </button>
</template>

<script>
export default {
  mounted() {
    // 挂载后自动聚焦
    this.$refs.myInput.focus()
  },
  methods: {
    focusInput() {
      this.$refs.myInput.focus()
    }
  }
}
</script>
import { useRef, useEffect } from 'react'

function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null)

  // mounted: 自动聚焦
  useEffect(() => {
    inputRef.current?!.focus()
  }, [])

  const focusInput = () => {
    inputRef.current?!.focus()
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>
        聚焦
      </button>
    </>
  )
}
// 祖先组件:提供数据
export default {
  provide() {
    return {
      theme: this.theme,
      updateTheme: this.updateTheme
    }
  },
  data() {
    return { theme: 'light' }
  },
  methods: {
    updateTheme(val) {
      this.theme = val
    }
  }
}

// 后代组件:注入数据
export default {
  inject: ['theme', 'updateTheme'],
  template: `
    <div :class="theme">
      <button @click="updateTheme('dark')">
        切换主题
      </button>
    </div>
  `
}
import React, {
  createContext, useContext, useState
} from 'react'

// 1. 创建 Context
interface ThemeContextType {
  theme: string
  updateTheme: (val: string) => void
}

const ThemeContext = createContext<
  ThemeContextType
>({ theme: 'light', updateTheme: () => {} })

// 2. 祖先组件:Provider
function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider
      value={{ theme, updateTheme: setTheme }}
    >
      <Child />
    </ThemeContext.Provider>
  )
}

// 3. 后代组件:useContext
function Child() {
  const { theme, updateTheme } =
    useContext(ThemeContext)

  return (
    <div className={theme}>
      <button
        onClick={() => updateTheme('dark')}
      >
        切换主题
      </button>
    </div>
  )
}

🧠 心智模型转换

迁移时需要牢记的核心思维差异

🟢
Vue 2 思维
响应式代理
  • 修改数据即触发视图更新
  • 模板是声明式的 HTML 扩展
  • 单文件组件 (.vue) 三段式结构
  • Options API 组织代码
  • 框架管理副作用的生命周期
🔵
React 思维
不可变数据
  • 状态不可变,必须通过 setter 更新
  • JSX 是 JavaScript 的语法扩展
  • 一个函数即一个组件
  • Hooks 逻辑复用更灵活
  • 开发者显式管理副作用依赖
记住这一点

Vue 帮你做了很多(响应式追踪、依赖收集、自动更新),而 React 把控制权交给你。 这意味着 React 的心智负担更集中在"何时重新执行"上, 但换来了更可预测的数据流和更好的可调试性。

← 返回概览 下一章:组件开发迁移 →