Vue 2 vs React 18 深度对比指南

Vue 2 vs React 18 深度对比指南

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


目录

  1. 核心设计哲学
  2. 组件定义
  3. 响应式原理
  4. [模板 vs JSX](#模板 vs JSX "#4-%E6%A8%A1%E6%9D%BF-vs-jsx")
  5. 生命周期对比
  6. [计算属性 vs useMemo](#计算属性 vs useMemo "#6-%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7-vs-usememo")
  7. [侦听器 vs useEffect](#侦听器 vs useEffect "#7-%E4%BE%A6%E5%90%AC%E5%99%A8-vs-useeffect")
  8. 父子通信
  9. 跨层级通信
  10. [插槽 vs children / render props](#插槽 vs children / render props "#10-%E6%8F%92%E6%A7%BD-vs-children--render-props")
  11. [虚拟 DOM 与 Diff 算法](#虚拟 DOM 与 Diff 算法 "#11-%E8%99%9A%E6%8B%9F-dom-%E4%B8%8E-diff-%E7%AE%97%E6%B3%95")
  12. [React 18 新特性](#React 18 新特性 "#12-react-18-%E6%96%B0%E7%89%B9%E6%80%A7")
  13. 性能优化对比
  14. 总结对照表
  15. 迁移建议

1. 核心设计哲学

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

2. 组件定义

Vue 2:选项式 API

vue 复制代码
<template>
  <div>{{ count }}</div>
</template>

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

React 18:函数组件 + Hooks

tsx 复制代码
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 的依赖追踪

text 复制代码
数据变化流程:
data → defineProperty(getter/setter)
       ↓
   getter 收集依赖(Watcher)
       ↓
   setter 触发依赖更新
       ↓
   Watcher 通知组件重新渲染
核心代码逻辑(简化)
js 复制代码
// 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:不可变数据 + 调度更新

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

核心机制:

js 复制代码
// 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)

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

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

4. 模板 vs JSX

Vue 2:模板 + 指令

vue 复制代码
<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

tsx 复制代码
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-for array.map()
双向绑定 v-model 受控组件(value + onChange)
事件 @click onClick
样式 :class / :style className / style={{}}

5. 生命周期对比

Vue 2 生命周期

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

React 18:useEffect 统一处理

tsx 复制代码
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 2 React 18
created 函数体顶部(但要注意 SSR)
mounted useEffect(() => {}, [])
updated useEffect(() => {})useEffect(() => {}, [deps])
beforeDestroy useEffect 返回的清理函数
watch useEffect(() => {}, [watchedValue])

6. 计算属性 vs useMemo

Vue 2:computed

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

原理:

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

React 18:useMemo

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

原理:

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

关键区别

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

7. 侦听器 vs useEffect

Vue 2:watch

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

React 18:useEffect

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

获取旧值的方式:

tsx 复制代码
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

vue 复制代码
<!-- 父组件 -->
<template>
  <Child :msg="message" @update="handleUpdate" />
</template>

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

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

React 18

tsx 复制代码
// 父组件
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 2 React 18
父→子 props props
子→父 $emit 回调函数 props
双向绑定 v-model / .sync 受控组件模式

9. 跨层级通信

Vue 2:provide / inject

js 复制代码
// 祖先组件
export default {
  provide() {
    return {
      theme: this.theme
    }
  }
}

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

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


React 18:Context

tsx 复制代码
// 创建 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:插槽

vue 复制代码
<!-- 父组件 -->
<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

tsx 复制代码
// 父组件
<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 2 React 18
默认插槽 <slot /> children
具名插槽 <slot name="x" /> 具名 props(如 header
作用域插槽 render props(函数作为 props)

11. 虚拟 DOM 与 Diff 算法

Vue 2 Diff

  • 双端比较算法:同时从新旧节点列表的两端向中间比较
  • 优化:静态节点标记,跳过不变的节点
text 复制代码
旧: [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 建立映射
text 复制代码
旧: [A, B, C, D]
新: [D, A, B, C]

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

关键:key 的作用

tsx 复制代码
// ❌ 错误:用 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 最大的变化:渲染可以被中断。

tsx 复制代码
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 数据获取

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

12.3 自动批处理

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

13. 性能优化对比

Vue 2 性能优化

js 复制代码
// 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 性能优化

tsx 复制代码
// 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 2 React 18
组件缓存 自动(响应式追踪) 手动(React.memo
计算缓存 computed(自动依赖) useMemo(手动依赖)
函数缓存 不需要(方法绑定在实例上) useCallback(避免子组件重渲染)
组件保活 <keep-alive> 无内置,需第三方库

14. 总结对照表

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

15. 迁移建议

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

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

js 复制代码
// Vue:直接修改
this.list.push(item)

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

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

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

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

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

js 复制代码
// Vue:this.handleClick 始终是同一个函数
methods: {
  handleClick() { ... }
}

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

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

Vue 2 React 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 延迟更新

相关推荐
QT 小鲜肉7 分钟前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
羽沢311 小时前
ECharts 学习
前端·学习·echarts
LYFlied1 小时前
WebAssembly (Wasm) 跨端方案深度解析
前端·职场和发展·wasm·跨端
七月丶1 小时前
实战复盘:我为什么把 TypeScript 写的 CLI 工具用 Rust 重写了一遍?
前端·后端·rust
over6971 小时前
《闭包、RAG与AI面试官:一个前端程序员的奇幻LangChain之旅》
前端·面试·langchain
JIngJaneIL1 小时前
基于java+ vue交友系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·交友
苹果酱05671 小时前
解决linux mysql命令 bash: mysql: command not found 的方法
java·vue.js·spring boot·mysql·课程设计
拉不动的猪1 小时前
回顾计算属性的缓存与监听的触发返回结果
前端·javascript·vue.js
karshey2 小时前
【IOS webview】h5页面播放视频时,IOS系统显示设置的icon
前端·ios