3.Composition API与响应式系统

查看飞书原文 ↗5,834 字符
​
✏️
Composition API 是 Vue 3 最重要的新特性,也是面试中的高频考点。它不仅改变了我们组织代码的方式,还为逻辑复用提供了更优雅的解决方案。掌握它,你就掌握了 Vue 3 的精髓。​
​
​
​
为什么需要 Composition API​
核心 API 详解​
1. ref() - 创建基础类型的响应式引用​
2. reactive() - 创建对象的响应式代理​
3. computed() - 计算属性​
computed 用于创建一个根据其他响应式数据派生出来的值。它具有缓存特性。​
基本用法​
import { ref, computed } from 'vue'

export default {
  setup() {
    const firstName = ref('John')
    const lastName = ref('Doe')

    // 1. 只读的计算属性 (传入一个 getter 函数)
    // 只有当 firstName.value 或 lastName.value 变化时才会重新计算
    const fullName = computed(() => `${firstName.value} ${lastName.value}`)

    // 2. 可写的计算属性 (传入一个包含 get 和 set 的对象)
    const editableName = computed({
      get: () => `${firstName.value} ${lastName.value}`,
      set: (newValue) => {
        // 当你尝试给 editableName.value 赋值时,set 函数会被调用
        const names = newValue.split(' ')
        firstName.value = names[0]
        lastName.value = names[1] || ''
      }
    })

    return {
      fullName,
      editableName
    }
  }
}
​
✏️
面试标准回答​
computed 用于创建计算属性,它会基于其响应式依赖进行缓存。这意味着只要依赖项没有发生变化,多次访问计算属性会立即返回之前计算过的结果,而不会重新执行计算函数。这是它与 methods 的核心区别。computed 可以是只读的,也可以通过提供 get 和 set 函数来创建可写的计算属性。​
​
​
​
​
​
4. watch() & watchEffect() - 侦听器​
watch:明确指定侦听源​
watch 需要明确指定要侦听的数据源,并在数据变化时执行回调函数。​
import { ref, reactive, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const state = reactive({ name: 'John' })

    // 1. 侦听单个 ref
    watch(count, (newVal, oldVal) => {
      console.log(`count 从 ${oldVal} 变为 ${newVal}`)
    })

    // 2. 侦听 getter 函数,可以访问对象属性
    watch(() => state.name, (newName, oldName) => {
      console.log(`name 变化了: ${newName}`)
    })
    
    // 3. 侦听多个源
    watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {
      console.log('count 或 name 发生了变化')
    })

    // 4. 选项:deep (深度侦听) 和 immediate (立即执行)
    watch(
      () => state,
      (newState, oldState) => { console.log('state 深度变化') },
      { deep: true, immediate: true } // immediate 会让 watch 在初始化时立即执行一次
    )
    
    return { count, state }
  }
}
​
​
watchEffect:自动追踪依赖​
watchEffect 会立即执行一次,并自动追踪回调函数中所有使用到的响应式依赖。当任何依赖发生变化时,它会重新运行。​
import { ref, watchEffect } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const message = ref('hello')

    // watchEffect 会立即执行,并自动追踪 count 和 message 的变化
    watchEffect(() => {
      // 在这个函数体内用到了 count.value 和 message.value
      // 所以 Vue 会自动侦听这两个响应式引用的变化
      console.log(`count: ${count.value}, message: ${message.value}`)
    })

    // 它等价于一个立即执行 (immediate: true) 且自动依赖收集的 watch
    
    return { count, message }
  }
}
对比​
​
特性​
watch​
watchEffect​
侦听源​
需要手动指定​
自动追踪依赖,无需指定​
​
​
​
​
​
​
​
​
​
​
​
​
​
执行时机​
​
​
默认在数据变化后执行​
​
​
立即执行一次,然后依赖变化后执行​
​
​
回调参数​
​
​
可以访问新值和旧值​
​
​
无法访问旧值​
​
​
使用场景​
​
​
需要精确控制侦听目标,或需要访问旧值时​
​
​
逻辑简单,只需依赖变化就执行副作用时​
​
​
​
✏️
面试标准回答​
watch 和 watchEffect 都用于在数据变化时执行副作用。主要区别在于:watch 需要显式指定侦听的数据源,让你能精确控制副作用的触发时机,并且可以访问到新值和旧值。而 watchEffect 会自动收集其回调函数中的响应式依赖,并在初始化时立即执行一次,代码更简洁,但无法访问旧值。​
​
​
​
​
响应式陷阱与解决方案​
陷阱 1:解构 reactive 对象导致失去响应性​
直接解构 reactive 对象会使其属性变为普通变量,从而失去响应性。​
import { reactive, toRefs } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0, name: 'John' })

    // ❌ 错误:count 和 name 只是普通变量,不再具有响应性
    // 当 state.count 改变时,这里的 count 不会更新
    const { count, name } = state

    // ✅ 正确:使用 toRefs()
    // toRefs 会将 reactive 对象的每个属性都转换为一个 ref
    const stateAsRefs = toRefs(state)
    // 现在 stateAsRefs.count 和 stateAsRefs.name 都是 ref,可以在模板中安全使用

    return {
      // 如果直接返回 count, 它不是响应式的
      // 必须返回 toRefs 转换后的结果
      ...stateAsRefs // 使用扩展运算符将 { count: ref, name: ref } 返回
    }
  }
}
​
​
陷阱 2:错误地替换响应式对象​
对于 ref 包裹的对象或数组,修改时必须通过 .value 属性进行。​
import { ref } from 'vue'

export default {
  setup() {
    let list = ref([1, 2, 3])

    const updateList = () => {
      // ❌ 错误:这会使 list 变量指向一个新的普通数组,原来的响应式引用丢失了
      // list = [4, 5, 6] 

      // ✅ 正确:始终通过 .value 来修改 ref 的值
      list.value = [4, 5, 6]
    }
    return { list, updateList }
  }
}
​
​
与 React Hooks 对比​
​
特性​
Vue Composition API​
React Hooks​
执行时机​
setup 函数只在组件创建时执行一次。​
Hooks 函数在每次组件渲染时都会执行。​
响应式系统​
基于 Proxy 的响应式系统,数据变更自动触发更新。​
​
需要通过 useState 的 set 函数手动更新状态,触发重渲染。​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
依赖管理​
​
​
computed 和 watchEffect 自动追踪依赖,无需手动声明。​
​
​
​
useMemo, useCallback 需要手动管理依赖数组,否则可能导致性能问题或陈旧闭包。​
​
​
心智负担​
​
​
心智负担较低,更接近原生 JavaScript 的编程体验。​
​
​
心智负担较高,需要理解闭包、依赖数组等规则。​
​
​
​
​
高级应用:自定义组合函数​
组合函数是 Composition API 的精髓,它允许我们将可复用的有状态逻辑封装起来。​
示例:设计一个 useFetch 组合函数​
// composables/useFetch.js

import { ref, watchEffect, toValue } from 'vue'

// 一个组合函数就是一个返回响应式状态的普通函数
export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  const fetchData = async () => {
    // 重置状态
    loading.value = true
    error.value = null
    
    try {
      // toValue 可以将 ref 或 getter 解包为普通值
      const response = await fetch(toValue(url))
      if (!response.ok) throw new Error('Network response was not ok')
      data.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }

  // 使用 watchEffect 侦听 URL 的变化
  // 如果 url 是一个 ref (例如 computed),当它变化时会自动重新 fetch
  watchEffect(fetchData)

  // 返回响应式状态和方法
  return { data, error, loading, refetch: fetchData }
}
​
使用示例​
// MyComponent.vue

import { ref, computed } from 'vue'
import { useFetch } from './composables/useFetch'

export default {
  setup() {
    const userId = ref(1)
    
    // 创建一个计算属性作为 URL,当 userId 变化时,URL 会自动更新
    const url = computed(() => `https://api.example.com/users/${userId.value}`)

    // 使用自定义的 useFetch Hook
    // 当 url 变化时,useFetch 内部的 watchEffect 会自动重新请求数据
    const { data: user, loading, error } = useFetch(url)

    const changeUser = () => {
      userId.value++ // 这会触发 url 的重新计算,进而触发 useFetch 重新执行
    }

    return { user, loading, error, changeUser }
  }
}
​