Chapter 01

Vue 3 核心理念与
Composition API

理解 Vue 3 为何重写、Composition API 解决了什么问题,以及 script setup 如何让代码更简洁

1.1 为什么需要 Vue 3?

Vue 2 自 2016 年发布以来,在全球范围内积累了大量用户。然而随着应用规模的扩大,Options API 的局限性也逐渐暴露。Vue 3 在 2020 年 9 月正式发布,是对整个框架的彻底重写,核心目标是:

版本说明

本教程基于 Vue 3.4+(代号 Slam Dunk)编写,充分利用最新的 defineModel()、编译器宏改进等特性。建议使用 Vite 5+ 作为构建工具。

1.2 Options API 的问题:逻辑碎片化

在 Vue 2 的 Options API 中,代码按照选项类型组织——数据放 data(),计算属性放 computed,方法放 methods,生命周期分散在各个钩子。当一个组件变得复杂时,同一功能(比如"搜索过滤")的相关代码会被拆散到多个选项中:

// Vue 2 写法 — 搜索逻辑被拆散到多处
export default {
  data() {
    return {
      searchQuery: '',       // 搜索相关
      items: [],               // 搜索相关
      isLoading: false,       // 搜索相关
      userName: '',           // 用户相关(混在一起)
    }
  },
  computed: {
    filteredItems() {          // 搜索相关
      return this.items.filter(i => i.includes(this.searchQuery))
    },
    greeting() {               // 用户相关
      return `你好,${this.userName}`
    }
  },
  methods: {
    async fetchItems() {       // 搜索相关
      this.isLoading = true
      this.items = await api()
      this.isLoading = false
    }
  },
  mounted() {
    this.fetchItems()           // 搜索相关
  }
}

这种结构导致阅读和维护一个功能时,需要不断在不同区域来回跳转。Vue 3 的 Composition API 让相同逻辑的代码聚合在一起

// Vue 3 写法 — 搜索逻辑集中,用户逻辑集中
import { ref, computed, onMounted } from 'vue'

// ── 搜索逻辑(可提取为 useSearch composable)──
const searchQuery = ref('')
const items = ref([])
const isLoading = ref(false)
const filteredItems = computed(() =>
  items.value.filter(i => i.includes(searchQuery.value))
)
async function fetchItems() {
  isLoading.value = true
  items.value = await api()
  isLoading.value = false
}

// ── 用户逻辑(清晰分离)──
const userName = ref('')
const greeting = computed(() => `你好,${userName.value}`)

onMounted(fetchItems)

1.3 setup() 函数:Composition API 的入口

setup() 是 Composition API 的执行入口,在组件实例创建之前调用,所有组合式逻辑都在这里定义。

setup(props, ctx)
接收 props 和上下文(attrs、slots、emit)。返回的对象暴露给模板。在 beforeCreate 之前执行,此时 this 不可用。
reactive context
setup 内部不能直接访问 this。需要通过 props 参数访问 prop,通过 ctx.emit 触发事件。
返回值
setup 返回的所有内容会暴露给模板、其他选项(computed/methods)和父组件的模板引用。
<!-- 标准 setup() 写法(非语法糖版本,适合理解原理)-->
<script>
import { ref, computed } from 'vue'

export default {
  name: 'Counter',
  props: { initialCount: Number },
  setup(props, { emit }) {
    const count = ref(props.initialCount ?? 0)
    const doubled = computed(() => count.value * 2)

    function increment() {
      count.value++
      emit('change', count.value)
    }

    return { count, doubled, increment } // 暴露给模板
  }
}
</script>

<template>
  <button @click="increment">{{ count }} (× 2 = {{ doubled }})</button>
</template>

1.4 <script setup>:编译器语法糖

<script setup> 是 Vue 3.2 引入的编译器语法糖,是目前官方推荐的单文件组件写法。它让代码更简洁:

<script setup lang="ts">
import { ref, computed } from 'vue'

// defineProps 是编译器宏,无需 import
const props = defineProps<{
  initialCount?: number
}>()

// defineEmits 同理
const emit = defineEmits<{
  change: [value: number]
}>()

// 所有顶层声明自动暴露给模板
const count = ref(props.initialCount ?? 0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
  emit('change', count.value)
}
</script>

<template>
  <button @click="increment">
    {{ count }} (× 2 = {{ doubled }})
  </button>
</template>

1.5 Vue 3 与 Vue 2 核心差异对照

特性Vue 2Vue 3
响应式实现Object.defineProperty(有缺陷)ES6 Proxy(全面、高效)
逻辑复用Mixins(命名冲突、来源不清晰)Composables(明确的来源与命名)
TypeScript类型支持差,需要 vue-class-component原生 TS 支持,script setup 完美推断
根节点只能有一个根元素支持 Fragment(多根节点)
全局 APIVue.use() / Vue.component()(全局污染)createApp() 实例隔离
v-model只能一个,prop 固定为 value支持多个 v-model,可自定义 prop 名
Teleport内置 <Teleport> 组件
Suspense内置 <Suspense> 异步组件支持
响应式 APIthis.$set / this.$delete(补丁方案)天然支持属性新增/删除

1.6 与 React Hooks 的对比

Composition API 受到 React Hooks 启发,但有本质差异:

React Hooks 限制

  • 必须在函数组件顶层调用(不能在条件/循环中)
  • 每次渲染都重新执行(性能开销)
  • 闭包陷阱(stale closure)
  • 需要手动管理依赖数组(useEffect deps)

Vue Composition API 优势

  • setup() 只执行一次,无重复执行问题
  • 响应式系统自动追踪依赖,无需手动声明
  • 无闭包陷阱(ref.value 始终最新)
  • 可以在条件语句中使用(无 Rules of Hooks)
// React — useEffect 需要手动写依赖数组,容易遗漏
useEffect(() => {
  fetchData(userId)  // 如果忘写 userId 会有 bug
}, [userId])        // 手动维护依赖数组

// Vue — watchEffect 自动追踪,不会遗漏
watchEffect(() => {
  fetchData(userId.value)  // 自动追踪 userId,无需声明
})

1.7 创建第一个 Vue 3 项目

使用官方脚手架工具快速创建项目:

# 使用 Vite 官方模板创建 Vue 3 + TypeScript 项目
npm create vue@latest my-app
# 交互式选择:TypeScript ✓  JSX ✗  Router ✓  Pinia ✓  Vitest ✓

cd my-app
npm install
npm run dev   # 启动开发服务器 http://localhost:5173
项目结构说明

src/main.ts 中使用 createApp(App).mount('#app') 替代了 Vue 2 的 new Vue()。每个 createApp 实例都是独立的,互不影响,非常适合在同一页面挂载多个 Vue 应用。

1.8 名词速查

SFC
Single File Component,单文件组件。一个 .vue 文件同时包含 template、script、style 三部分,是 Vue 特有的文件格式。
Composition API
组合式 API。Vue 3 引入的一组函数(ref、reactive、computed 等),允许开发者以函数组合的方式组织组件逻辑,取代 Options API 的选项式组织方式。
script setup
Vue 3.2+ 引入的编译器语法糖,写在 <script setup> 标签内的代码等价于 setup() 函数的内容,编译时自动处理 return。
编译器宏
Compiler Macros,如 defineProps、defineEmits、defineExpose 等。这些函数只在编译时有意义,会被 Vue 编译器转换,运行时实际上不存在这些函数调用。
Tree-shaking
摇树优化。构建工具(如 Vite/Webpack)在打包时自动删除未使用的代码。Vue 3 的 API 都是按需导入的函数,完美支持 Tree-shaking,而 Vue 2 通过 this.xxx 访问,难以优化。
Fragment
片段。Vue 3 允许组件模板有多个根节点,Vue 2 必须有单一根节点。这通过虚拟 DOM 级别的 Fragment 实现。
从 Vue 2 迁移注意

Vue 3 移除了 Vue.filter()Vue.prototype$listeners$scopedSlots 等。v-forv-if 的优先级也发生了变化(Vue 3 中 v-if 优先级更高)。迁移前建议阅读官方迁移指南。