Skip to content

Vue3 Composition API 实战指南

Composition API 是 Vue3 的核心特性,提供了更灵活的逻辑组织方式,让代码更易维护和复用。

🎯 Composition API 基础

setup 函数

vue
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  name: 'Counter',
  setup() {
    // 响应式数据
    const count = ref(0)
    const title = ref('计数器应用')

    // 计算属性
    const doubleCount = computed(() => count.value * 2)

    // 方法
    const increment = () => {
      count.value++
    }

    const decrement = () => {
      count.value--
    }

    // 生命周期
    onMounted(() => {
      console.log('组件已挂载')
    })

    // 返回模板需要的数据和方法
    return {
      title,
      count,
      doubleCount,
      increment,
      decrement
    }
  }
}
</script>

script setup 语法糖

vue
<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

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

// 直接声明响应式数据
const count = ref(0)
const title = ref('计数器应用')

// 计算属性
const doubleCount = computed(() => count.value * 2)

// 方法
const increment = () => {
  count.value++
}

const decrement = () => {
  count.value--
}

// 生命周期
onMounted(() => {
  console.log('组件已挂载')
})

// 无需 return,所有顶层变量自动暴露给模板
</script>

📊 响应式 API

ref 和 reactive

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

// ref - 基本类型响应式
const count = ref(0)
const message = ref('Hello Vue3')

// reactive - 对象响应式
const state = reactive({
  user: {
    name: 'Alice',
    age: 25
  },
  settings: {
    theme: 'dark',
    language: 'zh-CN'
  }
})

// 解构响应式对象
const { user, settings } = toRefs(state)

// 修改数据
const updateUser = () => {
  count.value++ // ref 需要 .value
  state.user.name = 'Bob' // reactive 直接修改
  user.value.age = 26 // 解构后需要 .value
}
</script>

computed 计算属性

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

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value) {
    const names = value.split(' ')
    firstName.value = names[0]
    lastName.value = names[names.length - 1]
  }
})

// 使用
const updateName = () => {
  fullNameWritable.value = 'Jane Smith'
}
</script>

watch 和 watchEffect

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

const count = ref(0)
const message = ref('')
const user = reactive({
  name: 'Alice',
  age: 25
})

// 监听单个 ref
watch(count, (newValue, oldValue) => {
  console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 监听多个源
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
  console.log('Multiple values changed')
})

// 监听响应式对象
watch(
  () => user.name,
  (newName, oldName) => {
    console.log(`User name changed from ${oldName} to ${newName}`)
  }
)

// 深度监听
watch(
  user,
  (newUser, oldUser) => {
    console.log('User object changed')
  },
  { deep: true }
)

// 立即执行监听
watch(
  count,
  (newValue) => {
    console.log('Count is:', newValue)
  },
  { immediate: true }
)

// watchEffect - 自动追踪依赖
watchEffect(() => {
  console.log(`Count is ${count.value}, message is ${message.value}`)
})

// 停止监听
const stopWatcher = watch(count, () => {
  console.log('Watching count')
})

// 在某个条件下停止监听
if (someCondition) {
  stopWatcher()
}
</script>

🔄 生命周期钩子

生命周期对比

vue
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue'

// beforeCreate 和 created 在 setup 中不需要
// setup 本身就在这两个钩子之间执行

onBeforeMount(() => {
  console.log('组件挂载前')
})

onMounted(() => {
  console.log('组件已挂载')
  // DOM 操作
  document.title = 'Vue3 应用'
})

onBeforeUpdate(() => {
  console.log('组件更新前')
})

onUpdated(() => {
  console.log('组件已更新')
})

onBeforeUnmount(() => {
  console.log('组件卸载前')
  // 清理工作
})

onUnmounted(() => {
  console.log('组件已卸载')
})

onErrorCaptured((error, instance, info) => {
  console.error('捕获到错误:', error)
  return false // 阻止错误继续传播
})
</script>

🎨 组件通信

Props 和 Emits

vue
<!-- 子组件 UserCard.vue -->
<template>
  <div class="user-card">
    <h3>{{ user.name }}</h3>
    <p>Age: {{ user.age }}</p>
    <button @click="handleEdit">编辑</button>
    <button @click="handleDelete">删除</button>
  </div>
</template>

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

// 定义 Props
const props = defineProps({
  user: {
    type: Object,
    required: true,
    validator: (user) => {
      return user && typeof user.name === 'string' && typeof user.age === 'number'
    }
  },
  editable: {
    type: Boolean,
    default: true
  }
})

// 定义 Emits
const emit = defineEmits(['edit', 'delete'])

// 使用 Props
const displayName = computed(() => {
  return props.user.name.toUpperCase()
})

// 触发事件
const handleEdit = () => {
  if (props.editable) {
    emit('edit', props.user.id)
  }
}

const handleDelete = () => {
  emit('delete', props.user.id)
}
</script>
vue
<!-- 父组件 -->
<template>
  <div>
    <UserCard
      v-for="user in users"
      :key="user.id"
      :user="user"
      :editable="true"
      @edit="handleEditUser"
      @delete="handleDeleteUser"
    />
  </div>
</template>

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

const users = ref([
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 }
])

const handleEditUser = (userId) => {
  console.log('编辑用户:', userId)
}

const handleDeleteUser = (userId) => {
  users.value = users.value.filter(user => user.id !== userId)
}
</script>

provide/inject

vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const user = ref({ name: 'Alice', role: 'admin' })

// 提供数据
provide('theme', theme)
provide('user', user)
provide('updateTheme', (newTheme) => {
  theme.value = newTheme
})
</script>
vue
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

// 注入数据
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')

// 使用默认值
const language = inject('language', 'zh-CN')

// 使用
const toggleTheme = () => {
  const newTheme = theme.value === 'dark' ? 'light' : 'dark'
  updateTheme(newTheme)
}
</script>

🔧 实用技巧

模板引用

vue
<template>
  <div>
    <input ref="inputRef" type="text" />
    <button @click="focusInput">聚焦输入框</button>
    
    <ChildComponent ref="childRef" />
    <button @click="callChildMethod">调用子组件方法</button>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 模板引用
const inputRef = ref(null)
const childRef = ref(null)

const focusInput = () => {
  inputRef.value.focus()
}

const callChildMethod = () => {
  childRef.value.someMethod()
}

onMounted(() => {
  // 组件挂载后可以访问 DOM
  console.log(inputRef.value) // <input> 元素
})
</script>

动态组件

vue
<template>
  <div>
    <button
      v-for="tab in tabs"
      :key="tab.name"
      @click="currentTab = tab.component"
      :class="{ active: currentTab === tab.component }"
    >
      {{ tab.name }}
    </button>
    
    <component :is="currentTab" />
  </div>
</template>

<script setup>
import { ref, shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'

// 使用 shallowRef 优化性能
const currentTab = shallowRef(TabA)

const tabs = [
  { name: 'Tab A', component: TabA },
  { name: 'Tab B', component: TabB },
  { name: 'Tab C', component: TabC }
]
</script>

异步组件

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

// 简单异步组件
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'))

// 高级异步组件
const AdvancedAsyncComponent = defineAsyncComponent({
  loader: () => import('./AdvancedComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <div>
    <AsyncComponent />
    <AdvancedAsyncComponent />
  </div>
</template>

🎯 性能优化

响应式优化

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

// 浅层响应式 - 只有根级别属性是响应式的
const shallowState = shallowRef({
  nested: {
    count: 0
  }
})

// 浅层响应式对象
const shallowObj = shallowReactive({
  nested: {
    count: 0
  }
})

// 只读数据
const readonlyState = readonly({
  config: {
    apiUrl: 'https://api.example.com'
  }
})

// 大型不可变数据使用 shallowRef
const largeData = shallowRef({
  // 大量数据...
})

// 更新整个对象而不是修改属性
const updateLargeData = (newData) => {
  largeData.value = newData
}
</script>

计算属性缓存

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

const list = ref([1, 2, 3, 4, 5])

// 昂贵的计算会被缓存
const expensiveValue = computed(() => {
  console.log('执行昂贵计算')
  return list.value.reduce((sum, item) => {
    // 模拟昂贵计算
    for (let i = 0; i < 1000000; i++) {}
    return sum + item
  }, 0)
})

// 只有当 list 改变时才会重新计算
</script>

💡 最佳实践

1. 逻辑组织

vue
<script setup>
// 1. 导入
import { ref, computed, watch, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'

// 2. 组合式函数
const { user, login, logout } = useAuth()
const { theme, toggleTheme } = useTheme()

// 3. 响应式数据
const loading = ref(false)
const error = ref(null)

// 4. 计算属性
const isLoggedIn = computed(() => !!user.value)

// 5. 方法
const handleLogin = async () => {
  loading.value = true
  try {
    await login()
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
}

// 6. 监听器
watch(user, (newUser) => {
  if (newUser) {
    // 用户登录后的逻辑
  }
})

// 7. 生命周期
onMounted(() => {
  // 初始化逻辑
})
</script>

2. 类型安全

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

interface User {
  id: number
  name: string
  email: string
}

// 类型化的 ref
const user = ref<User | null>(null)
const users = ref<User[]>([])

// 类型化的计算属性
const userCount = computed((): number => {
  return users.value.length
})

// 类型化的方法
const addUser = (newUser: User): void => {
  users.value.push(newUser)
}
</script>

🔧 Composition API 源码实现原理

setup 函数执行机制

javascript
// Vue3 组件初始化过程中的 setup 调用
function setupComponent(instance, isSSR = false) {
  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)

  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined

  return setupResult
}

function setupStatefulComponent(instance, isSSR) {
  const Component = instance.type
  const { setup } = Component

  if (setup) {
    const setupContext = createSetupContext(instance)

    // 设置当前实例
    setCurrentInstance(instance)

    // 暂停依赖收集
    pauseTracking()

    // 调用 setup 函数
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [instance.props, setupContext]
    )

    // 恢复依赖收集
    resetTracking()

    // 清除当前实例
    unsetCurrentInstance()

    if (isPromise(setupResult)) {
      // 异步组件处理
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
      return setupResult
        .then((resolvedResult) => {
          handleSetupResult(instance, resolvedResult, isSSR)
        })
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  }
}

ref 实现原理

javascript
class RefImpl {
  private _value: T
  private _rawValue: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)

    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

function trackRefValue(ref) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffects(ref.dep || (ref.dep = createDep()))
  }
}

function triggerRefValue(ref, newVal) {
  ref = toRaw(ref)
  if (ref.dep) {
    triggerEffects(ref.dep)
  }
}

computed 实现原理

javascript
class ComputedRefImpl {
  public dep?: Dep = undefined
  private _value!: T
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

🎨 高级模式与技巧

1. 响应式解构

javascript
import { toRefs, toRef } from 'vue'

// 解构响应式对象
const state = reactive({
  count: 0,
  name: 'Vue3'
})

// ❌ 直接解构会失去响应性
const { count, name } = state

// ✅ 使用 toRefs 保持响应性
const { count, name } = toRefs(state)

// ✅ 单个属性使用 toRef
const count = toRef(state, 'count')

2. 响应式转换

javascript
import { unref, toRaw, markRaw, isRef, isReactive } from 'vue'

const state = reactive({ count: 0 })
const countRef = ref(10)

// 获取原始值
console.log(unref(countRef)) // 10,等同于 countRef.value
console.log(toRaw(state)) // 获取原始对象

// 标记为非响应式
const nonReactive = markRaw({
  foo: 'bar'
})

// 类型检查
console.log(isRef(countRef)) // true
console.log(isReactive(state)) // true

3. 效果作用域

javascript
import { effectScope, onScopeDispose } from 'vue'

function useFeature() {
  const scope = effectScope()

  scope.run(() => {
    const count = ref(0)

    watch(count, () => {
      console.log('count changed')
    })

    // 作用域销毁时清理
    onScopeDispose(() => {
      console.log('scope disposed')
    })
  })

  // 手动停止作用域
  const stop = () => {
    scope.stop()
  }

  return { stop }
}

4. 自定义响应式

javascript
// 自定义 ref
function customRef(factory) {
  return new CustomRefImpl(factory)
}

class CustomRefImpl {
  public dep?: Dep = undefined
  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']
  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    const { get, set } = factory(
      () => trackRefValue(this),
      () => triggerRefValue(this)
    )
    this._get = get
    this._set = set
  }

  get value() {
    return this._get()
  }

  set value(newVal) {
    this._set(newVal)
  }
}

// 使用示例:防抖 ref
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

🔄 与 Options API 对比

逻辑组织对比

javascript
// Options API - 按选项类型组织
export default {
  data() {
    return {
      count: 0,
      loading: false,
      error: null
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    },
    async fetchData() {
      this.loading = true
      try {
        // fetch logic
      } catch (err) {
        this.error = err
      } finally {
        this.loading = false
      }
    }
  },
  mounted() {
    this.fetchData()
  }
}

// Composition API - 按逻辑功能组织
export default {
  setup() {
    // 计数器逻辑
    const { count, doubleCount, increment } = useCounter()

    // 数据获取逻辑
    const { loading, error, fetchData } = useAsyncData()

    onMounted(() => {
      fetchData()
    })

    return {
      count,
      doubleCount,
      increment,
      loading,
      error
    }
  }
}

代码复用对比

javascript
// Options API - 使用 mixin(存在命名冲突风险)
const counterMixin = {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

export default {
  mixins: [counterMixin],
  // 可能存在命名冲突
}

// Composition API - 使用组合式函数(更清晰的依赖关系)
function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  return { count, increment }
}

export default {
  setup() {
    const { count, increment } = useCounter(10)
    return { count, increment }
  }
}

🎯 性能优化深度解析

1. 响应式性能优化

javascript
// 避免不必要的响应式转换
const expensiveData = shallowRef({
  // 大量数据,只需要根级别响应式
  items: new Array(10000).fill(0).map((_, i) => ({ id: i, value: Math.random() }))
})

// 使用 markRaw 标记不需要响应式的对象
const chart = markRaw(new Chart(canvas, config))

// 合理使用 readonly
const config = readonly({
  apiUrl: 'https://api.example.com',
  timeout: 5000
})

2. 计算属性优化

javascript
// 避免在计算属性中进行昂贵操作
const expensiveList = ref([...])

// ❌ 每次都重新计算
const processedList = computed(() => {
  return expensiveList.value.map(item => {
    // 昂贵的处理逻辑
    return processItem(item)
  })
})

// ✅ 使用缓存优化
const processedList = computed(() => {
  const cache = new Map()
  return expensiveList.value.map(item => {
    if (cache.has(item.id)) {
      return cache.get(item.id)
    }
    const processed = processItem(item)
    cache.set(item.id, processed)
    return processed
  })
})

3. 监听器优化

javascript
// 使用 flush: 'post' 在 DOM 更新后执行
watch(
  source,
  callback,
  { flush: 'post' }
)

// 使用 watchEffect 的清理函数
watchEffect((onInvalidate) => {
  const token = performAsyncOperation()

  onInvalidate(() => {
    // 清理异步操作
    token.cancel()
  })
})

Composition API 不仅提供了更灵活的代码组织方式,其底层实现也体现了Vue3在性能和开发体验上的显著提升。通过深入理解其原理和最佳实践,能够更好地发挥Vue3的优势。


下一步:学习 Vue3 自定义 Hooks