发布于

Vue 3 Composition API深度指南:响应式系统与组合式函数实战

作者

Vue 3 Composition API深度指南:响应式系统与组合式函数实战

Vue 3的Composition API为组件逻辑组织提供了更灵活的方式。本文将深入探讨Composition API的核心概念和实战应用技巧。

Composition API基础

响应式系统核心

<template>
  <div class="counter-demo">
    <h2>计数器演示</h2>
    <div class="counter-display">
      <span class="count">{{ count }}</span>
      <span class="double-count">双倍: {{ doubleCount }}</span>
    </div>
    
    <div class="controls">
      <button @click="increment">增加</button>
      <button @click="decrement">减少</button>
      <button @click="reset">重置</button>
    </div>
    
    <div class="info">
      <p>点击次数: {{ clickCount }}</p>
      <p>最后操作: {{ lastOperation }}</p>
      <p>是否为偶数: {{ isEven ? '是' : '否' }}</p>
    </div>
  </div>
</template>

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

// 基础响应式引用
const count = ref<number>(0)
const clickCount = ref<number>(0)
const lastOperation = ref<string>('无')

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

// 响应式对象
const state = reactive({
  history: [] as string[],
  maxValue: 0,
  minValue: 0
})

// 解构响应式对象
const { history, maxValue, minValue } = toRefs(state)

// 方法定义
const increment = () => {
  count.value++
  clickCount.value++
  lastOperation.value = '增加'
  state.history.push(`增加到 ${count.value}`)
  
  if (count.value > state.maxValue) {
    state.maxValue = count.value
  }
}

const decrement = () => {
  count.value--
  clickCount.value++
  lastOperation.value = '减少'
  state.history.push(`减少到 ${count.value}`)
  
  if (count.value < state.minValue) {
    state.minValue = count.value
  }
}

const reset = () => {
  count.value = 0
  clickCount.value++
  lastOperation.value = '重置'
  state.history.push('重置为 0')
}

// 监听器
watch(count, (newValue, oldValue) => {
  console.log(`计数从 ${oldValue} 变为 ${newValue}`)
})

// 监听多个值
watch([count, clickCount], ([newCount, newClickCount], [oldCount, oldClickCount]) => {
  console.log('多值监听:', {
    count: { old: oldCount, new: newCount },
    clicks: { old: oldClickCount, new: newClickCount }
  })
})

// 立即执行的监听器
watchEffect(() => {
  document.title = `计数器: ${count.value}`
})

// 深度监听响应式对象
watch(state, (newState) => {
  console.log('状态变化:', newState)
}, { deep: true })
</script>

<style scoped>
.counter-demo {
  max-width: 400px;
  margin: 0 auto;
  padding: 2rem;
  border: 1px solid #ddd;
  border-radius: 8px;
}

.counter-display {
  display: flex;
  justify-content: space-between;
  margin: 1rem 0;
  padding: 1rem;
  background: #f5f5f5;
  border-radius: 4px;
}

.count {
  font-size: 2rem;
  font-weight: bold;
  color: #007bff;
}

.controls {
  display: flex;
  gap: 0.5rem;
  margin: 1rem 0;
}

.controls button {
  flex: 1;
  padding: 0.5rem;
  border: none;
  border-radius: 4px;
  background: #007bff;
  color: white;
  cursor: pointer;
}

.controls button:hover {
  background: #0056b3;
}

.info {
  margin-top: 1rem;
  padding: 1rem;
  background: #e9ecef;
  border-radius: 4px;
}

.info p {
  margin: 0.5rem 0;
}
</style>

响应式API详解

// composables/useReactivity.ts
import { 
  ref, 
  reactive, 
  computed, 
  watch, 
  watchEffect,
  readonly,
  shallowRef,
  shallowReactive,
  toRef,
  toRefs,
  unref,
  isRef,
  isReactive,
  isReadonly,
  markRaw,
  nextTick
} from 'vue'

// 响应式API使用示例
export function useReactivityDemo() {
  // 基础ref
  const count = ref(0)
  const message = ref('Hello Vue 3')
  
  // 对象ref
  const user = ref({
    name: 'John',
    age: 30,
    profile: {
      email: 'john@example.com',
      avatar: 'avatar.jpg'
    }
  })
  
  // reactive对象
  const state = reactive({
    loading: false,
    error: null as string | null,
    data: [] as any[],
    filters: {
      search: '',
      category: 'all',
      sortBy: 'name'
    }
  })
  
  // 只读响应式
  const readonlyState = readonly(state)
  
  // 浅层响应式
  const shallowState = shallowReactive({
    surface: 'reactive',
    nested: {
      deep: 'not reactive'
    }
  })
  
  // 浅层ref
  const shallowCount = shallowRef({ count: 0 })
  
  // 计算属性
  const doubleCount = computed(() => count.value * 2)
  
  // 可写计算属性
  const fullName = computed({
    get: () => `${user.value.name} (${user.value.age})`,
    set: (value: string) => {
      const [name, age] = value.split(' (')
      user.value.name = name
      user.value.age = parseInt(age.replace(')', ''))
    }
  })
  
  // 监听器选项
  const stopWatcher = watch(
    count,
    (newValue, oldValue) => {
      console.log(`Count changed: ${oldValue} -> ${newValue}`)
    },
    {
      immediate: true,    // 立即执行
      deep: true,        // 深度监听
      flush: 'post'      // DOM更新后执行
    }
  )
  
  // 监听多个源
  watch(
    [count, message],
    ([newCount, newMessage], [oldCount, oldMessage]) => {
      console.log('Multiple sources changed')
    }
  )
  
  // 监听响应式对象的特定属性
  watch(
    () => state.filters.search,
    (newSearch) => {
      console.log('Search filter changed:', newSearch)
    }
  )
  
  // watchEffect自动收集依赖
  const stopEffect = watchEffect(() => {
    console.log(`Current count: ${count.value}, message: ${message.value}`)
  })
  
  // 异步watchEffect
  watchEffect(async (onInvalidate) => {
    const response = await fetch(`/api/data?count=${count.value}`)
    
    // 清理函数
    onInvalidate(() => {
      console.log('Effect invalidated')
    })
    
    const data = await response.json()
    state.data = data
  })
  
  // toRef和toRefs
  const searchRef = toRef(state.filters, 'search')
  const filtersRefs = toRefs(state.filters)
  
  // 工具函数
  const increment = () => {
    count.value++
  }
  
  const updateUser = (updates: Partial<typeof user.value>) => {
    Object.assign(user.value, updates)
  }
  
  const toggleLoading = () => {
    state.loading = !state.loading
  }
  
  // 检查响应式类型
  const checkReactivity = () => {
    console.log('isRef(count):', isRef(count))
    console.log('isReactive(state):', isReactive(state))
    console.log('isReadonly(readonlyState):', isReadonly(readonlyState))
  }
  
  // 标记非响应式
  const nonReactiveData = markRaw({
    heavyData: new Array(1000000).fill(0)
  })
  
  // 清理函数
  const cleanup = () => {
    stopWatcher()
    stopEffect()
  }
  
  return {
    // 响应式数据
    count,
    message,
    user,
    state,
    readonlyState,
    shallowState,
    shallowCount,
    
    // 计算属性
    doubleCount,
    fullName,
    
    // 解构的refs
    searchRef,
    ...filtersRefs,
    
    // 方法
    increment,
    updateUser,
    toggleLoading,
    checkReactivity,
    cleanup,
    
    // 非响应式数据
    nonReactiveData
  }
}

// 响应式工具函数
export function useReactiveUtils() {
  // 安全的unref
  const safeUnref = <T>(val: T | Ref<T>): T => {
    return unref(val)
  }
  
  // 创建可选的响应式引用
  const maybeRef = <T>(val: T | Ref<T>): Ref<T> => {
    return isRef(val) ? val : ref(val)
  }
  
  // 创建切换状态的ref
  const useToggle = (initialValue = false) => {
    const state = ref(initialValue)
    
    const toggle = (value?: boolean) => {
      state.value = typeof value === 'boolean' ? value : !state.value
    }
    
    return [state, toggle] as const
  }
  
  // 创建计数器
  const useCounter = (initialValue = 0) => {
    const count = ref(initialValue)
    
    const increment = (delta = 1) => {
      count.value += delta
    }
    
    const decrement = (delta = 1) => {
      count.value -= delta
    }
    
    const reset = () => {
      count.value = initialValue
    }
    
    return {
      count: readonly(count),
      increment,
      decrement,
      reset
    }
  }
  
  // 创建异步状态管理
  const useAsyncState = <T>(
    promise: Promise<T>,
    initialState: T,
    options: {
      resetOnExecute?: boolean
      shallow?: boolean
    } = {}
  ) => {
    const { resetOnExecute = true, shallow = true } = options
    
    const state = shallow ? shallowRef(initialState) : ref(initialState)
    const isReady = ref(false)
    const isLoading = ref(false)
    const error = ref<Error | null>(null)
    
    const execute = async () => {
      error.value = null
      isReady.value = false
      isLoading.value = true
      
      if (resetOnExecute) {
        state.value = initialState
      }
      
      try {
        const data = await promise
        state.value = data
        isReady.value = true
      } catch (err) {
        error.value = err as Error
      } finally {
        isLoading.value = false
      }
    }
    
    execute()
    
    return {
      state: readonly(state),
      isReady: readonly(isReady),
      isLoading: readonly(isLoading),
      error: readonly(error),
      execute
    }
  }
  
  return {
    safeUnref,
    maybeRef,
    useToggle,
    useCounter,
    useAsyncState
  }
}

组合式函数实战

自定义Hooks开发

// composables/useLocalStorage.ts
import { ref, watch, Ref } from 'vue'

export function useLocalStorage<T>(
  key: string,
  defaultValue: T,
  options: {
    serializer?: {
      read: (value: string) => T
      write: (value: T) => string
    }
    syncAcrossTabs?: boolean
  } = {}
): [Ref<T>, (value: T) => void, () => void] {
  const {
    serializer = {
      read: JSON.parse,
      write: JSON.stringify
    },
    syncAcrossTabs = true
  } = options
  
  // 读取初始值
  const storedValue = localStorage.getItem(key)
  const initialValue = storedValue !== null 
    ? serializer.read(storedValue)
    : defaultValue
  
  const state = ref<T>(initialValue)
  
  // 写入localStorage
  const setValue = (value: T) => {
    try {
      state.value = value
      localStorage.setItem(key, serializer.write(value))
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error)
    }
  }
  
  // 删除localStorage
  const removeValue = () => {
    try {
      localStorage.removeItem(key)
      state.value = defaultValue
    } catch (error) {
      console.error(`Error removing localStorage key "${key}":`, error)
    }
  }
  
  // 监听值变化
  watch(
    state,
    (newValue) => {
      setValue(newValue)
    },
    { deep: true }
  )
  
  // 跨标签页同步
  if (syncAcrossTabs) {
    window.addEventListener('storage', (e) => {
      if (e.key === key && e.newValue !== null) {
        try {
          state.value = serializer.read(e.newValue)
        } catch (error) {
          console.error(`Error syncing localStorage key "${key}":`, error)
        }
      }
    })
  }
  
  return [state, setValue, removeValue]
}

// composables/useFetch.ts
import { ref, reactive, toRefs } from 'vue'

interface UseFetchOptions {
  immediate?: boolean
  refetch?: boolean
  initialData?: any
  timeout?: number
  beforeFetch?: (ctx: { url: string; options: RequestInit; cancel: () => void }) => Promise<void> | void
  afterFetch?: (ctx: { data: any; response: Response }) => any
  onFetchError?: (ctx: { data: any; error: Error; response?: Response }) => void
}

export function useFetch<T = any>(
  url: string,
  options: UseFetchOptions = {}
) {
  const {
    immediate = true,
    refetch = false,
    initialData = null,
    timeout = 0,
    beforeFetch,
    afterFetch,
    onFetchError
  } = options
  
  const state = reactive({
    data: initialData as T | null,
    error: null as Error | null,
    isFetching: false,
    canAbort: false,
    statusCode: null as number | null,
    response: null as Response | null
  })
  
  let controller: AbortController | null = null
  
  const abort = () => {
    if (controller) {
      controller.abort()
      controller = null
      state.canAbort = false
    }
  }
  
  const execute = async (throwOnFailed = false) => {
    abort()
    
    controller = new AbortController()
    state.isFetching = true
    state.canAbort = true
    state.error = null
    
    const fetchOptions: RequestInit = {
      signal: controller.signal
    }
    
    // 超时处理
    if (timeout > 0) {
      setTimeout(() => {
        if (controller) {
          controller.abort()
        }
      }, timeout)
    }
    
    try {
      // beforeFetch钩子
      if (beforeFetch) {
        await beforeFetch({
          url,
          options: fetchOptions,
          cancel: abort
        })
      }
      
      const response = await fetch(url, fetchOptions)
      state.response = response
      state.statusCode = response.status
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      let data = await response.json()
      
      // afterFetch钩子
      if (afterFetch) {
        data = afterFetch({ data, response }) || data
      }
      
      state.data = data
      
      return data
    } catch (error) {
      const fetchError = error as Error
      state.error = fetchError
      
      // onFetchError钩子
      if (onFetchError) {
        onFetchError({
          data: state.data,
          error: fetchError,
          response: state.response || undefined
        })
      }
      
      if (throwOnFailed) {
        throw fetchError
      }
      
      return null
    } finally {
      state.isFetching = false
      state.canAbort = false
      controller = null
    }
  }
  
  // 立即执行
  if (immediate) {
    execute()
  }
  
  return {
    ...toRefs(state),
    execute,
    abort,
    refetch: () => execute()
  }
}

// composables/useIntersectionObserver.ts
import { ref, watch, unref, type Ref } from 'vue'

export function useIntersectionObserver(
  target: Ref<Element | null> | Element | null,
  callback: IntersectionObserverCallback,
  options: IntersectionObserverInit = {}
) {
  const isSupported = window && 'IntersectionObserver' in window
  
  const isIntersecting = ref(false)
  const isActive = ref(false)
  
  let observer: IntersectionObserver | null = null
  
  const cleanup = () => {
    if (observer) {
      observer.disconnect()
      observer = null
      isActive.value = false
    }
  }
  
  const start = () => {
    if (!isSupported) return
    
    cleanup()
    
    const element = unref(target)
    if (!element) return
    
    observer = new IntersectionObserver(
      (entries) => {
        isIntersecting.value = entries[0].isIntersecting
        callback(entries, observer!)
      },
      options
    )
    
    observer.observe(element)
    isActive.value = true
  }
  
  const stop = cleanup
  
  // 监听target变化
  watch(
    () => unref(target),
    (newTarget) => {
      if (newTarget) {
        start()
      } else {
        stop()
      }
    },
    { immediate: true }
  )
  
  return {
    isSupported,
    isIntersecting,
    isActive,
    start,
    stop
  }
}

总结

Vue 3 Composition API的核心要点:

  1. 响应式系统:ref、reactive、computed、watch等API的深度应用
  2. 组合式函数:逻辑复用、状态管理、异步处理的最佳实践
  3. 生命周期钩子:组件生命周期的精确控制和优化
  4. 性能优化:防抖节流、虚拟滚动、内存管理等技巧
  5. TypeScript集成:类型安全的组合式函数开发

Composition API为Vue 3带来了更强大的逻辑组织能力,通过合理使用这些特性,可以构建出更加灵活、可维护的Vue应用。