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

相关推荐
Howie Zphile36 分钟前
NEXTJS/REACT有哪些主流的UI可选
前端·react.js·ui
fruge38 分钟前
React Server Components 实战:下一代 SSR 开发指南
前端·javascript·react.js
lichong95139 分钟前
harmonyos 大屏设备怎么弹出 u 盘
前端·macos·华为·typescript·android studio·harmonyos·大前端
irises39 分钟前
从零实现2D绘图引擎:5.5.简单图表demo
前端·数据可视化
irises39 分钟前
从零实现2D绘图引擎:5.鼠标悬停事件
前端·数据可视化
青莲84339 分钟前
Android Lifecycle 完全指南:从设计原理到生产实践
android·前端
irises41 分钟前
从零实现2D绘图引擎:4.矩形与文本的实现
前端·数据可视化
irises42 分钟前
从零实现2D绘图引擎:2.Storage和Painter的实现
前端·数据可视化
irises1 小时前
从零实现2D绘图引擎:3.交互系统(Handle)的实现
前端·数据可视化