Vue 2 vs React 18 深度对比指南

117 阅读9分钟

本文档面向熟练使用 Vue 2 的开发者,帮助快速理解 React 18 的核心概念与差异。

目标:Vue 2 选手 快速开发 React 项目

目录

  1. 核心设计哲学
  2. 组件定义
  3. 响应式原理
  4. 模板 vs JSX
  5. 生命周期对比
  6. 计算属性 vs useMemo
  7. 侦听器 vs useEffect
  8. 父子通信
  9. 跨层级通信
  10. 插槽 vs children / render props
  11. 虚拟 DOM 与 Diff 算法
  12. React 18 新特性
  13. 性能优化对比
  14. 总结对照表
  15. 迁移建议

1. 核心设计哲学

维度Vue 2React 18
定位渐进式框架(框架帮你做更多)UI 库(你自己组合生态)
模板模板语法 + 指令JSX(JS 的语法扩展)
响应式自动依赖追踪(getter/setter)手动声明更新(setState)
心智模型"数据变了,视图自动变""调用更新函数,触发重新渲染"

2. 组件定义

Vue 2:选项式 API

<template>
  <div>{{ count }}</div>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

React 18:函数组件 + Hooks

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  
  const increment = () => setCount(c => c + 1)
  
  return <div>{count}</div>
}

原理差异

  • Vue 2:组件是一个"配置对象",Vue 内部实例化并管理生命周期。this 指向组件实例,data 会被 Vue 用 Object.defineProperty 转成响应式。
  • React 18:组件就是一个"纯函数",每次渲染都会重新执行。状态通过 Hooks 保存在 React 内部的 Fiber 节点上,而不是组件实例上。

3. 响应式原理

Vue 2:基于 Object.defineProperty 的依赖追踪

数据变化流程:
data → defineProperty(getter/setter)
       ↓
   getter 收集依赖(Watcher)
       ↓
   setter 触发依赖更新
       ↓
   Watcher 通知组件重新渲染

核心代码逻辑(简化)

// Vue 2 响应式核心
function defineReactive(obj, key, val) {
  const dep = new Dep() // 依赖收集器
  
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend() // 收集当前 Watcher
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify() // 通知所有 Watcher 更新
    }
  })
}

局限性:

  • 无法检测属性的添加/删除(需要 Vue.set
  • 无法检测数组索引赋值(需要用 splice 等变异方法)

React 18:不可变数据 + 调度更新

状态变化流程:
setState(newValue)
    ↓
React 调度器标记组件需要更新
    ↓
批量处理更新(Batching)
    ↓
重新执行函数组件
    ↓
Diff 虚拟 DOM → 更新真实 DOM

核心机制:

// React 状态更新(简化)
function useState(initialValue) {
  // 状态存储在 Fiber 节点的 memoizedState 链表上
  const hook = mountWorkInProgressHook()
  hook.memoizedState = initialValue
  
  const dispatch = (action) => {
    // 创建更新对象,加入更新队列
    const update = { action, next: null }
    enqueueUpdate(hook.queue, update)
    // 调度更新
    scheduleUpdateOnFiber(fiber)
  }
  
  return [hook.memoizedState, dispatch]
}

React 18 新特性:自动批处理(Automatic Batching)

// React 17:只有事件处理函数内会批处理
// React 18:所有更新都会自动批处理

// 以下三次 setState 只会触发一次重渲染
setTimeout(() => {
  setCount(c => c + 1)
  setFlag(f => !f)
  setName('new')
}, 1000)

4. 模板 vs JSX

Vue 2:模板 + 指令

<template>
  <div>
    <!-- 条件渲染 -->
    <span v-if="show">显示</span>
    <span v-else>隐藏</span>
    
    <!-- 列表渲染 -->
    <ul>
      <li v-for="item in list" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    
    <!-- 双向绑定 -->
    <input v-model="text" />
    
    <!-- 事件 -->
    <button @click="handleClick">点击</button>
  </div>
</template>

原理:

  • 模板在编译阶段被转换为渲染函数(render function
  • 指令(v-ifv-for)是编译时的语法糖
  • 编译器可以做静态分析优化(标记静态节点)

React 18:JSX

function MyComponent({ show, list, text, setText }) {
  return (
    <div>
      {/* 条件渲染 */}
      {show ? <span>显示</span> : <span>隐藏</span>}
      
      {/* 列表渲染 */}
      <ul>
        {list.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
      
      {/* 受控组件(双向绑定) */}
      <input value={text} onChange={e => setText(e.target.value)} />
      
      {/* 事件 */}
      <button onClick={handleClick}>点击</button>
    </div>
  )
}

原理:

  • JSX 是 React.createElement() 的语法糖
  • 编译后:<div>hello</div>React.createElement('div', null, 'hello')
  • 没有指令,一切都是 JS 表达式

语法对照表

特性Vue 2 模板React JSX
条件v-if / v-else{condition && ...} 或三元
循环v-forarray.map()
双向绑定v-model受控组件(value + onChange)
事件@clickonClick
样式:class / :styleclassName / style={{}}

5. 生命周期对比

Vue 2 生命周期

beforeCreate → created → beforeMount → mounted
                              ↓
                        beforeUpdate → updated
                              ↓
                        beforeDestroy → destroyed
export default {
  created() {
    // 实例创建完成,data/methods 可用,DOM 未挂载
    // 常用于:初始化数据、发起请求
  },
  mounted() {
    // DOM 已挂载
    // 常用于:操作 DOM、初始化第三方库
  },
  updated() {
    // 数据变化导致 DOM 更新后
  },
  beforeDestroy() {
    // 销毁前,清理定时器、事件监听等
  }
}

React 18:useEffect 统一处理

import { useEffect, useLayoutEffect } from 'react'

function MyComponent() {
  // 相当于 mounted + updated
  useEffect(() => {
    console.log('组件挂载或更新后')
    
    // 相当于 beforeDestroy
    return () => {
      console.log('清理:组件卸载前 或 下次 effect 执行前')
    }
  }) // 无依赖数组:每次渲染后都执行
  
  // 相当于 mounted(只执行一次)
  useEffect(() => {
    console.log('只在挂载时执行')
    return () => console.log('只在卸载时执行')
  }, []) // 空依赖数组
  
  // 相当于 watch
  useEffect(() => {
    console.log('count 变化了')
  }, [count]) // 依赖 count
  
  return <div>...</div>
}

原理:

  • useEffect 的回调在 DOM 更新后异步执行(不阻塞渲染)
  • useLayoutEffectDOM 更新后同步执行(阻塞渲染,用于测量 DOM)
  • 依赖数组决定何时重新执行 effect

生命周期对照表

Vue 2React 18
created函数体顶部(但要注意 SSR)
mounteduseEffect(() => {}, [])
updateduseEffect(() => {})useEffect(() => {}, [deps])
beforeDestroyuseEffect 返回的清理函数
watchuseEffect(() => {}, [watchedValue])

6. 计算属性 vs useMemo

Vue 2:computed

export default {
  data() {
    return { firstName: 'John', lastName: 'Doe' }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`
    }
  }
}

原理:

  • computed 是一个惰性求值的 Watcher
  • 只有依赖变化时才重新计算
  • 有缓存:多次访问不会重复计算

React 18:useMemo

function MyComponent({ firstName, lastName }) {
  const fullName = useMemo(() => {
    return `${firstName} ${lastName}`
  }, [firstName, lastName])
  
  return <div>{fullName}</div>
}

原理:

  • useMemo 在依赖数组不变时返回缓存值
  • 依赖数组变化时重新执行计算函数
  • 必须手动声明依赖(Vue 是自动追踪)

关键区别

特性Vue 2 computedReact useMemo
依赖追踪自动手动声明
缓存
用途派生状态派生状态 + 避免重复计算

7. 侦听器 vs useEffect

Vue 2:watch

export default {
  data() {
    return { query: '' }
  },
  watch: {
    query: {
      handler(newVal, oldVal) {
        this.search(newVal)
      },
      immediate: true, // 立即执行
      deep: true       // 深度监听
    }
  }
}

React 18:useEffect

function SearchComponent() {
  const [query, setQuery] = useState('')
  
  useEffect(() => {
    // 没有 oldVal,需要自己用 useRef 保存
    search(query)
  }, [query])
  
  return <input value={query} onChange={e => setQuery(e.target.value)} />
}

获取旧值的方式:

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>()
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

// 使用
const prevQuery = usePrevious(query)

8. 父子通信

Vue 2

<!-- 父组件 -->
<template>
  <Child :msg="message" @update="handleUpdate" />
</template>

<!-- 子组件 -->
<template>
  <div @click="$emit('update', newValue)">{{ msg }}</div>
</template>

<script>
export default {
  props: ['msg']
}
</script>

React 18

// 父组件
function Parent() {
  const [message, setMessage] = useState('')
  
  return (
    <Child 
      msg={message} 
      onUpdate={(newValue) => setMessage(newValue)} 
    />
  )
}

// 子组件
interface ChildProps {
  msg: string
  onUpdate: (value: string) => void
}

function Child({ msg, onUpdate }: ChildProps) {
  return <div onClick={() => onUpdate('new')}>{msg}</div>
}

通信方式对照

特性Vue 2React 18
父→子propsprops
子→父$emit回调函数 props
双向绑定v-model / .sync受控组件模式

9. 跨层级通信

Vue 2:provide / inject

// 祖先组件
export default {
  provide() {
    return {
      theme: this.theme
    }
  }
}

// 后代组件
export default {
  inject: ['theme']
}

注意: Vue 2 的 provide/inject 不是响应式的(除非 provide 一个响应式对象)。


React 18:Context

// 创建 Context
const ThemeContext = createContext<string>('light')

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

// 后代组件
function DeepChild() {
  const theme = useContext(ThemeContext)
  return <div>当前主题:{theme}</div>
}

原理

  • Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染
  • 这是 React 的一个性能陷阱:Context 变化会导致所有消费者重渲染,即使它们只用了 Context 的一部分

优化方式:

  • 拆分 Context(读写分离)
  • 使用 useMemo 包裹 value
  • 或使用状态管理库(Redux/Zustand)

10. 插槽 vs children / render props

Vue 2:插槽

<!-- 父组件 -->
<Card>
  <template #header>标题</template>
  <template #default>内容</template>
  <template #footer="{ data }">{{ data }}</template>
</Card>

<!-- Card 组件 -->
<template>
  <div>
    <header><slot name="header" /></header>
    <main><slot /></main>
    <footer><slot name="footer" :data="footerData" /></footer>
  </div>
</template>

React 18:children + render props

// 父组件
<Card
  header={<span>标题</span>}
  footer={(data) => <span>{data}</span>}
>
  内容
</Card>

// Card 组件
interface CardProps {
  header?: ReactNode
  footer?: (data: string) => ReactNode
  children: ReactNode
}

function Card({ header, footer, children }: CardProps) {
  const footerData = 'some data'
  
  return (
    <div>
      <header>{header}</header>
      <main>{children}</main>
      <footer>{footer?.(footerData)}</footer>
    </div>
  )
}

插槽对照

Vue 2React 18
默认插槽 <slot />children
具名插槽 <slot name="x" />具名 props(如 header
作用域插槽render props(函数作为 props)

11. 虚拟 DOM 与 Diff 算法

Vue 2 Diff

  • 双端比较算法:同时从新旧节点列表的两端向中间比较
  • 优化:静态节点标记,跳过不变的节点
旧: [A, B, C, D]
新: [D, A, B, C]

双端比较:
1. 旧头(A) vs 新头(D) ❌
2. 旧尾(D) vs 新尾(C) ❌
3. 旧头(A) vs 新尾(C) ❌
4. 旧尾(D) vs 新头(D) ✅ → 移动 D 到最前

React 18 Diff

  • 单向遍历 + key 映射
  • 只从左到右遍历,通过 key 建立映射
旧: [A, B, C, D]
新: [D, A, B, C]

1. 遍历新列表,D 在旧列表中找到,但位置不对
2. 标记需要移动的节点
3. 最小化 DOM 操作

关键:key 的作用

// ❌ 错误:用 index 作为 key
{list.map((item, index) => <Item key={index} />)}

// ✅ 正确:用唯一标识作为 key
{list.map(item => <Item key={item.id} />)}

12. React 18 新特性

12.1 并发渲染(Concurrent Rendering)

React 18 最大的变化:渲染可以被中断。

import { useTransition, useDeferredValue } from 'react'

function SearchResults() {
  const [query, setQuery] = useState('')
  const [isPending, startTransition] = useTransition()
  
  const handleChange = (e) => {
    // 紧急更新:输入框立即响应
    setQuery(e.target.value)
    
    // 非紧急更新:搜索结果可以延迟
    startTransition(() => {
      setSearchResults(search(e.target.value))
    })
  }
  
  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <Results />}
    </>
  )
}

原理:

  • React 18 引入了优先级调度
  • startTransition 标记的更新是低优先级的,可以被高优先级更新打断
  • 用户输入等交互是高优先级,数据渲染是低优先级

12.2 Suspense 数据获取

// 配合 React Query / SWR / Relay 等
function ProfilePage() {
  return (
    <Suspense fallback={<Spinner />}>
      <ProfileDetails />
      <Suspense fallback={<PostsSpinner />}>
        <ProfilePosts />
      </Suspense>
    </Suspense>
  )
}

12.3 自动批处理

// React 18:所有更新自动批处理
setTimeout(() => {
  setCount(c => c + 1)  // 不会立即渲染
  setFlag(f => !f)      // 不会立即渲染
  // 只触发一次渲染
}, 1000)

13. 性能优化对比

Vue 2 性能优化

// 1. v-once:只渲染一次
<span v-once>{{ staticContent }}</span>

// 2. v-memo(Vue 3.2+,Vue 2 没有)

// 3. computed 自带缓存

// 4. keep-alive 缓存组件
<keep-alive>
  <component :is="currentComponent" />
</keep-alive>

React 18 性能优化

// 1. React.memo:组件级别缓存
const MemoizedComponent = React.memo(function MyComponent(props) {
  return <div>{props.value}</div>
})

// 2. useMemo:值缓存
const expensiveValue = useMemo(() => compute(a, b), [a, b])

// 3. useCallback:函数缓存
const handleClick = useCallback(() => {
  doSomething(a, b)
}, [a, b])

// 4. 懒加载
const LazyComponent = React.lazy(() => import('./HeavyComponent'))

<Suspense fallback={<Loading />}>
  <LazyComponent />
</Suspense>

优化方式对照

优化点Vue 2React 18
组件缓存自动(响应式追踪)手动(React.memo
计算缓存computed(自动依赖)useMemo(手动依赖)
函数缓存不需要(方法绑定在实例上)useCallback(避免子组件重渲染)
组件保活<keep-alive>无内置,需第三方库

14. 总结对照表

特性Vue 2React 18
组件定义选项式对象函数 + Hooks
响应式自动(defineProperty)手动(setState)
模板模板 + 指令JSX
状态data()useState
计算属性computeduseMemo
侦听watchuseEffect
生命周期多个钩子函数useEffect 统一
父子通信props + $emitprops + 回调
跨层级provide/injectContext
插槽slotchildren / render props
性能优化框架自动优化多开发者手动优化多
并发Concurrent Mode
学习曲线平缓陡峭(Hooks 心智模型)

15. 迁移建议

从 Vue 2 转 React 18,重点转变思维:

15.1 从"响应式"到"不可变"

// Vue:直接修改
this.list.push(item)

// React:创建新数组
setList([...list, item])

15.2 从"自动依赖"到"手动声明"

// Vue:computed 自动追踪依赖
computed: {
  fullName() {
    return this.firstName + this.lastName
  }
}

// React:useMemo 必须手动写依赖数组
const fullName = useMemo(() => {
  return firstName + lastName
}, [firstName, lastName])

15.3 从"实例方法"到"闭包函数"

// Vue:this.handleClick 始终是同一个函数
methods: {
  handleClick() { ... }
}

// React:每次渲染 handleClick 都是新函数(需要 useCallback 优化)
const handleClick = useCallback(() => {
  doSomething(a, b)
}, [a, b])

15.4 从"模板指令"到"JS 表达式"

Vue 2React 18
v-if="show"{show && <Component />}
v-for="item in list"{list.map(item => ...)}
v-model="value"value={value} onChange={...}

附录:常用 Hooks 速查

Hook用途Vue 2 对应
useState状态管理data
useEffect副作用处理mounted / watch / beforeDestroy
useMemo计算缓存computed
useCallback函数缓存无(Vue 不需要)
useRef引用 DOM / 保存可变值$refs / 实例属性
useContext跨层级状态inject
useReducer复杂状态逻辑Vuex mutations
useLayoutEffect同步副作用mounted(同步部分)
useTransition并发更新
useDeferredValue延迟更新