Vue2和Vue3 区别

468 阅读4分钟

从 Vue2 到 Vue3:一个老Vue开发者的升级指南

最近和前端同事交流发现在 vue3 的项目中还是惯性的使用 vue2 的选项式写法。这篇文章将从实践角度分享我的学习心得,希望能帮助同样准备升级的你。

为什么要升级到 Vue3?

在开始之前,我们先聊聊为什么要升级到 Vue3:

  1. 更好的性能:Vue3 的虚拟 DOM 重写和 Tree-shaking 支持带来了更好的性能
  2. 组合式 API:解决了 Vue2 中组件逻辑复用的痛点
  3. 更好的 TypeScript 支持:Vue3 是用 TypeScript 重写的,提供了更好的类型推导
  4. 更小的包体积:得益于 Tree-shaking,你可以只打包用到的功能

快速上手指南

1. 创建应用的新方式

如果你习惯了 Vue2 的写法:

// Vue2 的写法
import Vue from 'vue'
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

在 Vue3 中变成了:

// Vue3 的写法
import { createApp } from 'vue'
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')

个人感受:这种改变实际上更符合直觉,创建应用然后挂载插件的方式更加线性和清晰。

2. 响应式系统的变化

在 Vue2 中,我们习惯了直接在 data 中定义数据:

// Vue2
export default {
  data() {
    return {
      count: 0,
      user: {
        name: '张三',
        age: 25
      }
    }
  }
}

Vue3 中需要使用 ref 或 reactive:

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

// 简单类型用 ref
const count = ref(0)

// 复杂对象用 reactive
const user = reactive({
  name: '张三',
  age: 25
})

// 注意:在模板中使用 ref 的值不需要 .value
</script>

<template>
  <div>{{ count }}</div>
  <div>{{ user.name }}</div>
</template>

个人最佳实践:

  • 优先使用 ref,因为它可以包装任何值
  • 只在确实需要对象响应式的场景使用 reactive
  • 记住在 JS 中需要用 .value 访问 ref 的值

3. 组合式 API 介绍

在 Vue3 中,我们可以用 Composition API 重写:

// Vue3 的 组合式 API
<script setup>
import { ref, onMounted } from 'vue'

// 状态集中管理
const userInfo = ref({})
const loading = ref(false)
const error = ref(null)

// 业务逻辑集中管理
const fetchUserInfo = async () => {
  loading.value = true
  try {
    userInfo.value = await api.getUserInfo()
  } catch (err) {
    error.value = err
  } finally {
    loading.value = false
  }
}

// 生命周期钩子
onMounted(() => {
  fetchUserInfo()
})
</script>

为什么我推荐 Composition API:

  1. 相关逻辑可以组织在一起,不再需要在 data、methods、computed 之间来回跳转
  2. 更好的代码复用能力,可以轻松提取和复用逻辑
  3. 更好的类型推导支持

4. 模板语法的变化

Vue3 在模板语法方面保持了大部分与 Vue2 的兼容性,但有一些值得注意的改进:

<!-- Vue2 -->
<template>
  <div>
    <!-- v-for 和 v-if 优先级相同,不推荐一起使用 -->
    <div v-for="item in list" v-if="item.visible">
      {{ item.name }}
    </div>
    
    <!-- 只能有一个根节点 -->
    <div>
      <header></header>
      <main></main>
      <footer></footer>
    </div>
  </div>
</template>

<!-- Vue3 -->
<template>
  <!-- 可以有多个根节点 -->
  <header></header>
  <main>
    <!-- v-for 优先级高于 v-if,更符合直觉 -->
    <div v-for="item in list" :key="item.id">
      <div v-if="item.visible">{{ item.name }}</div>
    </div>
  </main>
  <footer></footer>
</template>

主要改进:

  • 支持多根节点模板
  • v-for 和 v-if 优先级更明确
  • 更好的 TypeScript 支持
  • 更好的性能优化

5. 生命周期的变化

Vue2 的生命周期钩子都有对应的 Vue3 版本,只是需要手动导入:

// Vue3
import { 
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted 
} from 'vue'

// 使用方式
onMounted(() => {
  console.log('组件已挂载')
})

注意:

  • beforeCreate 和 created 钩子在 setup 中是不需要的,因为 setup 本身就是在这两个钩子之间执行
  • destroyed 改名为 unmounted
  • beforeDestroy 改名为 beforeUnmount

6. 计算属性的使用

计算属性在 Vue3 中的写法更加简洁:

// Vue2
export default {
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    // 只读计算属性
    fullName() {
      return this.firstName + this.lastName
    },
    // 可写计算属性
    fullNameWithSetter: {
      get() {
        return this.firstName + this.lastName
      },
      set(newValue) {
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

// Vue3
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 只读计算属性
const fullName = computed(() => firstName.value + lastName.value)

// 可写计算属性
const fullNameWithSetter = computed({
  get: () => firstName.value + lastName.value,
  set: (newValue) => {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

7. 监听器的改进

Vue3 的 watch 和 watchEffect 提供了更强大的监听能力:

// Vue2
export default {
  data() {
    return {
      name: '',
      userInfo: {
        age: 25
      }
    }
  },
  watch: {
    // 监听简单属性
    name(newVal, oldVal) {
      console.log('name changed:', newVal, oldVal)
    },
    // 监听对象属性
    'userInfo.age': {
      handler(newVal, oldVal) {
        console.log('age changed:', newVal, oldVal)
      },
      deep: true,
      immediate: true
    }
  }
}

// Vue3
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'

const name = ref('')
const userInfo = reactive({
  age: 25
})

// 监听 ref
watch(name, (newVal, oldVal) => {
  console.log('name changed:', newVal, oldVal)
})

// 监听多个来源
watch([name, () => userInfo.age], ([newName, newAge], [oldName, oldAge]) => {
  console.log('values changed')
})

// 自动追踪依赖
watchEffect(() => {
  console.log(`name: ${name.value}, age: ${userInfo.age}`)
})
</script>

8. Props 和 Emits 的声明

Vue3 提供了更明确的 Props 和 Emits 声明方式:

// Vue2
export default {
  props: {
    title: {
      type: String,
      required: true,
      default: ''
    }
  },
  methods: {
    handleClick() {
      this.$emit('update', { value: 'newValue' })
    }
  }
}

// Vue3
<script setup>
import { defineProps, defineEmits } from 'vue'

// Props 类型声明
const props = defineProps({
  title: {
    type: String,
    required: true,
    default: ''
  }
})

// Emits 声明
const emit = defineEmits({
  // 带验证的事件声明
  update: (payload) => {
    if (payload.value) return true
    return false
  }
})

const handleClick = () => {
  emit('update', { value: 'newValue' })
}
</script>

<!-- 使用 TypeScript 时的声明方式 -->
<script setup lang="ts">
const props = defineProps<{
  title: string
  count?: number
}>()

const emit = defineEmits<{
  (e: 'update', payload: { value: string }): void
  (e: 'delete', id: number): void
}>()
</script>

Props 和 Emits 的主要改进:

  • 更好的类型推导
  • 运行时验证
  • 更清晰的事件类型定义
  • 可以使用 TypeScript 的类型标注

9. 插槽(Slots)的使用变化

// Vue2
<template>
  <div>
    <!-- 默认插槽 -->
    <slot></slot>
    
    <!-- 具名插槽 -->
    <slot name="header"></slot>
    
    <!-- 作用域插槽 -->
    <slot name="content" :data="data"></slot>
  </div>
</template>

// Vue3
<template>
  <!-- 默认插槽 - 基本相同 -->
  <slot></slot>
  
  <!-- 具名插槽 - 推荐使用 v-slot 指令 -->
  <slot name="header"></slot>
  
  <!-- 作用域插槽 - 更简洁的语法 -->
  <slot name="content" :data="data"></slot>
</template>

<!-- 使用方式 -->
<MyComponent>
  <!-- Vue3 中统一使用 v-slot 语法,不再推荐 slot-scope -->
  <template #default>默认内容</template>
  <template #header>头部内容</template>
  <template #content="{ data }">
    {{ data }}
  </template>
</MyComponent>

10. Teleport 组件

Vue3 新增了 Teleport 组件,可以将内容渲染到 DOM 的其他位置:

<!-- Vue3 新特性 -->
<template>
  <div class="modal-button">
    <button @click="showModal = true">打开模态框</button>

    <!-- 将模态框传送到 body 下 -->
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div>模态框内容</div>
        <button @click="showModal = false">关闭</button>
      </div>
    </Teleport>
  </div>
</template>

11. 异步组件的改进

// Vue2
const AsyncComp = () => ({
  component: import('./AsyncComp.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

// Vue3
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./AsyncComp.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

// 简写方式
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))

12. 全局 API 的改变

// Vue2
import Vue from 'vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

// Vue3
import { createApp } from 'vue'
const app = createApp(App)

app.config.compilerOptions.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

13. 自定义指令的变化

// Vue2
Vue.directive('highlight', {
  bind(el, binding) {},
  inserted(el, binding) {},
  update(el, binding) {},
  componentUpdated(el, binding) {},
  unbind(el, binding) {}
})

// Vue3
app.directive('highlight', {
  beforeMount(el, binding) {}, // 替代 bind
  mounted(el, binding) {},     // 替代 inserted
  beforeUpdate(el, binding) {}, // 新增
  updated(el, binding) {},     // 替代 update + componentUpdated
  beforeUnmount(el, binding) {}, // 新增
  unmounted(el, binding) {}    // 替代 unbind
})

14. 过渡动画的变化

<!-- Vue2 -->
<transition name="fade">
  <p v-if="show">hello</p>
</transition>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

<!-- Vue3 -->
<Transition name="fade">
  <p v-if="show">hello</p>
</Transition>

<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity .5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

主要变化:

  • 过渡类名改变:v-enter 改为 v-enter-from
  • 组件名称大写:transition 改为 Transition
  • 新增 persisted 钩子用于处理缓存组件的过渡

性能优化的改进

Vue3 在性能方面有显著提升:

  1. 更好的 Tree-shaking 支持

    • 按需引入 API
    • 更小的打包体积
  2. Fragment 支持

    • 不再需要根节点包裹
    • 减少 DOM 层级
  3. 静态提升

    • 静态节点被提升到渲染函数之外
    • 减少重复创建开销
  4. Proxy 响应式系统

    • 更好的性能
    • 更完善的响应式支持

升级建议

  1. 渐进式迁移

    • 新功能用 Vue3 + Composition API
    • 老项目可以继续使用 Options API,Vue3 完全兼容
    • 随着对 Composition API 的熟悉,逐步重构老代码
  2. 使用 <script setup>

    • 这是 Vue3 的推荐写法
    • 减少了模板引用的复杂度
    • 提供了更好的开发体验
  3. TypeScript 支持

    • 即使不用 TypeScript,也建议看看类型定义
    • 配合 VS Code,可以获得很好的类型提示
  4. 响应式系统使用建议

    • 优先使用 ref
    • 必要时使用 reactive
    • 注意 ref 的 .value 使用

结语

作为一个经历过从 Vue2 到 Vue3 迁移的开发者,我认为这次升级绝对值得。虽然学习曲线有点陡,但 Composition API 带来的好处远超过学习成本。建议大家在新项目中直接使用 Vue3,享受新版本带来的各种改进。

记住:Vue3 不是一个完全不同的框架,它是 Vue2 的自然进化,保持耐心,你会发现新的开发方式更加优雅和高效。