技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

想知道 Vue 3 与 Vue 2 的区别?整理版上手教程

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/78-想知道Vue3与Vue2的区别 五千字教程助你快速上手Vue3
本文目录10 个章节
  1. 一、选项式 API 与组合式 API
  2. 二、ref 和 reactive
  3. 三、生命周期
  4. 四、watch、computed 和 watchEffect
  5. 五、组件通信
  6. 六、v-model 和 .sync
  7. 七、路由相关写法
  8. 八、从 Vue 2 迁移到 Vue 3 的检查清单
  9. 总结
  10. 参考资料

想知道 Vue 3 与 Vue 2 的区别?整理版上手教程

原文从选项式 API、组合式 API、响应式、生命周期、watch/computed、组件通信、v-model 和路由几个方向比较 Vue 2 与 Vue 3。本文尽量保留原来的章节和示例,但把损坏的代码块、Vue 2/3 API 混用和不准确的结论整理为 Vue 2.7 与 Vue 3.5 都能对照理解的版本。

Vue 2.7 仍然适合维护历史项目,Vue 3.5 是新项目的主流版本。升级时不只是把 this 改成 ref,还要同时检查构建工具、插件、Router、状态管理、组件库和 SSR 框架的兼容性。

一、选项式 API 与组合式 API

两种 API 都可以完成同样的功能,主要区别在于组织代码的方式:

  • 选项式 API:按 datamethodscomputedwatch 和生命周期等选项组织;
  • 组合式 API:按一个功能所需的状态、计算值、副作用和方法组织;
  • <script setup>:组合式 API 的编译时语法糖,顶层绑定会自动暴露给同一组件的 template。

选项式 API 不是被 Vue 3 删除的旧 API,Vue 3 仍然支持它;组合式 API 也不是所有组件都必须使用的“唯一正确答案”。大型组件中组合式 API 更容易按功能复用和拆分,小型组件使用选项式 API 也可以很清晰。

1. 选项式 API

<template>
  <button @click="changeMsg">{{ msg }}</button>
</template>

<script>
export default {
  data() {
    return {
      msg: 'hello world',
    }
  },
  methods: {
    changeMsg() {
      this.msg = 'hello Vue'
    },
  },
}
</script>

2. 组合式 API

<template>
  <button @click="changeMsg">{{ msg }}</button>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const msg = ref('hello world')

    function changeMsg() {
      msg.value = 'hello Vue'
    }

    return { msg, changeMsg }
  },
})
</script>

3. <script setup>

<template>
  <button @click="changeMsg">{{ msg }}</button>
</template>

<script setup>
import { ref } from 'vue'

const msg = ref('hello world')

function changeMsg() {
  msg.value = 'hello Vue'
}
</script>

在普通 setup() 中,返回值才会暴露给 template;在 <script setup> 中,顶层声明由编译器自动暴露,不需要手写 return。组件、directive 和普通 import 也可以直接在模板中使用。

组合式 API 的优势不是“少写几行代码”,而是可以把相关逻辑放在一起,并通过 composable 复用:

// useCounter.ts
import { computed, ref } from 'vue'

export function useCounter() {
  const count = ref(0)
  const double = computed(() => count.value * 2)
  const increment = () => count.value++

  return { count, double, increment }
}

二、refreactive

Vue 2 中,data() 返回的对象会被 Vue 转换为响应式数据;Vue 3 的组合式 API 可以使用 ref()reactive() 显式创建响应式状态。

<script setup>
import { reactive, ref } from 'vue'

const msg = ref('hello world')
const user = reactive({
  name: 'Vue',
  age: 3,
})

function changeData() {
  msg.value = 'hello Vue'
  user.name = 'Vue 3'
}
</script>

ref

ref 可以保存基本值,也可以保存对象、数组:

const count = ref(0)
count.value++

const list = ref([{ id: 1 }])
list.value.push({ id: 2 })

在 JavaScript 中访问或修改 ref 通常需要 .value;在 Vue 模板中会自动解包顶层 ref:

<template>
  <button @click="count++">{{ count }}</button>
</template>

reactive

reactive 通常用于对象、数组和 Map/Set:

const state = reactive({
  message: 'hello',
  count: 0,
})

state.count++

reactive 有几个边界:

  • 只能接收对象、数组或集合,不能直接代理基本值;
  • 返回的是 Proxy,和原对象的 === 身份不同;
  • 不应把整个 reactive 变量替换成新对象,否则原有依赖仍指向旧代理;
  • 直接解构属性会失去该属性的响应式连接,可以使用 toRef/toRefs
  • 第三方实例或外部代理状态有时适合 shallowRefshallowReactivemarkRaw
import { reactive, toRefs } from 'vue'

const state = reactive({ message: 'Vue', count: 0 })
const { message, count } = toRefs(state)

“基本类型用 ref、复杂类型用 reactive”是便于入门的经验,不是硬性规则。ref 同样可以包装复杂对象,而且在 composable 返回状态时往往更灵活。

三、生命周期

Vue 3 的 Composition API 使用 onMounted 等 hook 注册生命周期;Vue 3 的 Options API 仍然支持 beforeCreatecreated 等选项。

Vue 2 Options APIVue 3 Options APIVue 3 Composition API说明
beforeCreatebeforeCreate无直接同名 hooksetup() 在组件创建早期执行
createdcreated无直接同名 hooksetup() 通常承接其中的状态初始化逻辑
beforeMountbeforeMountonBeforeMount首次 DOM 挂载前
mountedmountedonMounted组件 DOM 挂载后
beforeUpdatebeforeUpdateonBeforeUpdateDOM 更新提交前
updatedupdatedonUpdatedDOM 更新后
beforeDestroybeforeUnmountonBeforeUnmount卸载前
destroyedunmountedonUnmounted卸载完成
activatedactivatedonActivatedKeepAlive 激活
deactivateddeactivatedonDeactivatedKeepAlive 停用
errorCapturederrorCapturedonErrorCaptured捕获后代错误

Vue 3 还提供 onServerPrefetchonRenderTrackedonRenderTriggered 等 hook。SSR 中没有浏览器 DOM,因此 onMounted 不会在服务端执行。

onMounted 示例

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log('组件 DOM 已挂载')
})
</script>

setup() 不是一个“新的 created 钩子”,而是组件创建阶段执行的入口。Vue 3 中没有对应的 onBeforeCreate/onCreated Composition API hook,因为 setup 本身已经覆盖了这段初始化时机;如果使用 Options API,原有两个选项仍可使用。

四、watchcomputedwatchEffect

1. computed:计算派生状态

<script>
export default {
  data: () => ({ a: 1, b: 2 }),
  computed: {
    sum() {
      return this.a + this.b
    },
  },
}
</script>

Composition API:

import { computed, ref } from 'vue'

const a = ref(1)
const b = ref(2)
const sum = computed(() => a.value + b.value)

computed 默认惰性求值并缓存结果,只有依赖变化后才会重新计算。getter 应保持纯净,不要在其中发请求、修改其他状态或操作 DOM。

2. watch:监听明确数据源并执行副作用

import { ref, watch } from 'vue'

const a = ref(1)

const stop = watch(a, (newValue, oldValue) => {
  console.log(`${oldValue} -> ${newValue}`)
})

// 不需要时可以停止
// stop()

watch 的 source 可以是 ref、reactive 对象、getter 或 source 数组:

watch(
  () => route.params.id,
  id => loadArticle(String(id)),
  { immediate: true },
)

watch 默认不会立即执行;immediate: true 才会在注册时先执行一次。deepflushonce 等选项要根据场景使用,深度监听大型对象可能有明显成本。

3. watchEffect:自动追踪同步读取的依赖

<script setup>
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'

const watchTarget = ref(0)
let timer

const stop = watchEffect(() => {
  console.log(watchTarget.value)
})

onMounted(() => {
  timer = window.setInterval(() => {
    watchTarget.value++
  }, 1000)
})

onUnmounted(() => {
  if (timer) window.clearInterval(timer)
  stop()
})
</script>

watchEffect 会立即执行一次,并追踪回调中同步读取的响应式依赖;依赖变化后重新执行。它与 React useEffect 有相似之处,但调度、依赖收集和清理 API 不完全相同,也不需要依赖数组。异步回调中,首次 await 之后读取的依赖不会按同样方式自动追踪。

Vue 3.5 可以用 onWatcherCleanup 取消过期请求;较早版本也可以使用 watch 回调的第三个 onCleanup 参数:

import { onWatcherCleanup, watch } from 'vue'

watch(id, async value => {
  const controller = new AbortController()
  onWatcherCleanup(() => controller.abort())

  const response = await fetch(`/api/${value}`, {
    signal: controller.signal,
  })
  data.value = await response.json()
})

选择原则:

  • 要得到一个派生值:computed
  • 要监听明确数据源并做副作用:watch
  • 要自动追踪当前回调读取的依赖:watchEffect

五、组件通信

场景Vue 2 常见方式Vue 3 推荐方式
父传子propsprops
子传父$emitdefineEmits/emit
多层传递provide/injectprovide/inject
组件透传$attrs$listeners$attrs,未声明监听器也在其中
父访问子$refstemplate ref + defineExpose
兄弟/跨模块EventBus、共同父状态共同父状态、composable、Pinia、明确事件通道

1. props:父向子传递

父组件:

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const parentMsg = ref('父组件信息')
</script>

<template>
  <Child :msg="parentMsg" />
</template>

子组件:

<script setup>
const props = defineProps<{
  msg: string
}>()
</script>

<template>
  <p>{{ props.msg }}</p>
</template>

props 是单向下行绑定,子组件不应直接修改 prop。若需要编辑,应 emit 一个事件让父组件更新;若只是从 prop 派生本地值,可以复制到 ref,或用 computed 计算。

在 Vue 3.5 中,响应式 props destructure 已有编译器支持;为了兼容较早的 Vue 3 版本,仍可显式使用 toRef/toRefs

const props = defineProps<{ msg: string }>()
const msg = toRef(props, 'msg')

2. emits:子向父通知

子组件:

<script setup lang="ts">
const emit = defineEmits<{
  sendMsg: [value: string]
}>()

function send() {
  emit('sendMsg', '我是子组件数据')
}
</script>

<template>
  <button @click="send">发送</button>
</template>

父组件:

<Child @send-msg="value => console.log(value)" />

Options API 中仍可以使用:

export default {
  emits: ['sendMsg'],
  methods: {
    send() {
      this.$emit('sendMsg', '我是子组件数据')
    },
  },
}

3. $attrs$listeners

Vue 2 中,$attrs 通常表示没有被 props 声明的属性,$listeners 表示父组件传入的监听器。Vue 3 移除了 $listeners,未被 emits 声明的监听器会合并到 $attrs;class 和 style 也属于 fallthrough attrs 的处理范围。

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

<template>
  <!-- 将未声明的属性和监听器透传给真正的 input -->
  <input v-bind="attrs">
</template>

如果一个组件声明了 emits: ['sendMsg'],父组件的 @send-msg 就会被视为组件事件,而不是普通 $attrs。不建议通过 attrs.onParentFun() 主动调用父组件传来的函数,这会绕过 props/emits 的契约;需要子组件通知父组件时,应声明并 emit 事件。

需要控制透传目标时可以使用 inheritAttrs: false(Options API)或 defineOptions({ inheritAttrs: false })(Vue 3.3+),再显式 v-bind="$attrs"

4. provide/inject:跨层级依赖

适合主题、表单上下文、服务对象等跨层级依赖,不要把所有全局业务状态都藏进注入对象。

推荐使用 Symbol key:

// keys.ts
import type { InjectionKey, Ref } from 'vue'

export const messageKey: InjectionKey<Ref<string>> = Symbol('message')

提供方:

<script setup lang="ts">
import { provide, ref } from 'vue'
import { messageKey } from './keys'

const message = ref('父组件信息')
provide(messageKey, message)
</script>

后代组件:

<script setup lang="ts">
import { inject } from 'vue'
import { messageKey } from './keys'

const message = inject(messageKey)
</script>

<template>
  <p>{{ message }}</p>
</template>

注入一个 ref 时,ref 本身会保留响应式连接;Options API 中直接 provide() { return { msg: this.msg } } 传递普通字符串通常只是提供当时的值。需要响应式更新时,应提供 ref、reactive 对象或明确的方法。

5. $parent$children 和 template ref

Vue 2 的 $parent 可以访问父实例,$children 是直接子组件数组,但它不是响应式集合,也不应依赖其顺序。Vue 3 移除了 $children$parent 仍可能在公共实例上存在,但强耦合、难测试,不推荐作为常规通信方式。

父组件访问子组件的命令式能力应使用 template ref,并且只暴露必要 API:

<!-- Parent.vue -->
<script setup>
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'

const child = useTemplateRef('child')

onMounted(() => {
  child.value?.focusInput()
})
</script>

<template>
  <Child ref="child" />
</template>

useTemplateRef 是 Vue 3.5 的便利 API;较早版本可以使用 const child = ref(null) 并让模板 ref 使用同名变量。

<!-- Child.vue -->
<script setup>
import { ref } from 'vue'

const input = ref(null)

function focusInput() {
  input.value?.focus()
}

defineExpose({ focusInput })
</script>

<template>
  <input ref="input">
</template>

<script setup> 组件默认不会把所有顶层绑定作为公共实例 API 暴露给父组件,使用 defineExpose 可以明确控制边界。模板 ref 只有在挂载后才有 DOM/组件实例值,卸载后可能恢复为 null

6. EventBus 和 mitt

Vue 2 可以通过一个 Vue 实例的 $on/$emit/$off 做 EventBus;Vue 3 移除了组件实例事件 API,因此不能继续 new Vue() 作为总线。mitt 等第三方库仍然可以实现发布订阅,但它不是 Vue 内置替代品:

// bus.ts
import mitt from 'mitt'

export const bus = mitt<{
  'message': string
}>()
import { onUnmounted } from 'vue'
import { bus } from './bus'

function handleMessage(message: string) {
  console.log(message)
}

bus.on('message', handleMessage)
onUnmounted(() => {
  bus.off('message', handleMessage)
})

事件总线容易产生隐式依赖和忘记取消订阅的问题。兄弟组件优先提升状态到共同父组件;跨页面或业务共享状态优先考虑 Pinia;确实需要事件通道时,必须约定事件名、类型和生命周期。

六、v-model.sync

1. Vue 2 的组件 v-model

Vue 2 默认把组件上的:

<Child v-model="pageTitle" />

展开为:

<Child
  :value="pageTitle"
  @input="pageTitle = $event"
/>

也可以通过组件 model 选项改用其他 prop/event。Vue 2 的 .sync 常见写法是:

<Child :title.sync="pageTitle" />

它对应:

<Child
  :title="pageTitle"
  @update:title="pageTitle = $event"
/>

2. Vue 3 的组件 v-model

Vue 3 默认契约变为:

<Child v-model="pageTitle" />

等价于:

<Child
  :model-value="pageTitle"
  @update:model-value="pageTitle = $event"
/>

子组件可以这样实现:

<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

function onInput(event: Event) {
  emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>

<template>
  <input
    :value="props.modelValue"
    @input="onInput"
  >
</template>

3. 参数、多个模型和 defineModel

.sync 在 Vue 3 被移除,使用带参数的 v-model

<Child v-model:title="title" v-model:content="content" />

它分别对应 title/update:titlecontent/update:content。Vue 3.4+ 可以使用 defineModel 简化组件:

<script setup lang="ts">
const title = defineModel<string>('title', { required: true })
</script>

<template>
  <input v-model="title">
</template>

因此,v-model 并不是任意状态自动互相修改,而是 prop + update 事件的约定;组件仍保持清晰的父子数据流。

七、路由相关写法

Vue Router 3 和 Vue Router 4/5 的核心概念相近,但创建路由、守卫返回值和 Composition API 写法不同。

1. Options API 中访问路由

<script>
export default {
  methods: {
    async toPage() {
      await this.$router.push({ name: 'home' })
    },
  },
  created() {
    console.log(this.$route.params)
    console.log(this.$route.query)
  },
  beforeRouteLeave(to, from) {
    if (this.hasUnsavedChange) return false
  },
}
</script>

Vue Router 3 常见的组件守卫使用 next

beforeRouteLeave(to, from, next) {
  if (this.hasUnsavedChange) {
    next(false)
    return
  }
  next()
}

在 Vue Router 4/5 中,推荐返回 truefalse、重定向位置或抛出错误,不要在同一个守卫中混用返回值和 next

2. Composition API 中访问路由

<script setup lang="ts">
import {
  onBeforeRouteLeave,
  onBeforeRouteUpdate,
  useRoute,
  useRouter,
} from 'vue-router'
import { watch } from 'vue'

const router = useRouter()
const route = useRoute()

function toPage() {
  router.push({ name: 'home' })
}

watch(
  () => route.params.id,
  id => loadData(String(id)),
  { immediate: true },
)

onBeforeRouteUpdate(to => {
  console.log('参数变化:', to.params.id)
  return true
})

onBeforeRouteLeave(() => {
  if (hasUnsavedChange()) return false
  return true
})
</script>

useRoute() 返回响应式 route 对象;不要把整个 route 对象作为一个大 watch source,通常只监听需要的 route.params.idroute.query.keyword

beforeRouteEnter 是 Options API 组件守卫,因为它发生在组件实例创建前,所以没有对应的 onBeforeRouteEnter Composition API hook。需要在进入前校验可以使用路由记录的 beforeEnter、全局守卫或数据加载方案;进入组件后再根据参数请求数据则可以在 setupwatchonBeforeRouteUpdate 中完成。

3. Router 4/5 与 Router 3 的创建差异

Vue Router 3:

const router = new VueRouter({
  mode: 'history',
  routes,
})

Vue Router 4/5:

import {
  createRouter,
  createWebHistory,
} from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes,
})

Router 4/5 使用 router.addRoute() 动态添加一条路由,Router 3 历史项目中常见 addRoutes()。History 模式部署时需要服务器 fallback,Hash 模式使用 createWebHashHistory()

4. Nuxt 4 的补充

Nuxt 4 采用文件系统路由,页面通常放在 app/pages/,导航可以使用 Nuxt 的 navigateTo 或自动导入的路由组件。只有在明确使用 Vue Router 底层 API 时,才需要在 Nuxt 语境中手动创建 router;不要把纯 Vite Vue 项目的 main.ts 路由初始化代码原样复制到 Nuxt 项目。

八、从 Vue 2 迁移到 Vue 3 的检查清单

  1. 构建与入口:从 new Vue() 迁移到 createApp();检查 Vite、Webpack、插件和 vue-loader
  2. 响应式:识别 $set/$delete、数组特殊写法和直接解构响应式对象的代码;
  3. 生命周期beforeDestroy/destroyed 改为 beforeUnmount/unmounted
  4. 组件通信:移除 $listeners$children 和组件实例 EventBus;补充 emits、defineExpose
  5. v-modelvalue/input 改为 modelValue/update:modelValue.sync 改为带参数的 v-model
  6. 路由:Router 3 的 modenextaddRoutes 对照 Router 4/5 的 API 迁移;
  7. 状态管理:Vuex 4 可继续维护,新的 Vue 3 项目通常优先 Pinia;
  8. 第三方库:检查 UI 组件、指令、过滤器、过渡、SSR 和测试工具的 Vue 3 版本;
  9. 模板行为:检查 Fragment、多根节点、v-if/v-for 优先级、slot 和 $attrs 变化;
  10. SSR/部署:检查 hydration 一致性、请求级状态隔离和 History fallback。

总结

  • 选项式 API 和组合式 API 都是 Vue 的正式能力,区别主要在逻辑组织方式;
  • refreactive 都可以创建响应式状态,选择要结合替换、解构、返回 composable 和类型体验;
  • Vue 3 没有移除 Options API,只是 Composition API 使用 hook 注册生命周期和响应式逻辑;
  • computed 负责缓存的派生值,watchwatchEffect 负责副作用;
  • Vue 3 通过 props/emits、provide/inject、$attrs、template ref 和 Pinia 组织通信;
  • .sync 已由参数化 v-model 替代,Vue 3.4+ 还可以使用 defineModel
  • 路由组合式 API 使用 useRoute/useRouteronBeforeRouteLeave/onBeforeRouteUpdatebeforeRouteEnter 仍属于 Options API 语境;
  • Vue 2.7 和 Vue 3.5 可以并行维护,但 API、构建、插件和生态版本必须一起检查。

参考资料

原文作者:东方小月。原文关于 Options API、Composition API、ref/reactive、生命周期、watch/computed、组件通信、v-model 和路由的章节予以保留;格式损坏、错误的 Vue 3 生命周期结论、$attrs/$listeners 混淆、错误的 emits 写法、路由守卫 API 混用和推广噪声已清理或修正。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS