Vue.js侦听器(Composition API)

在 Vue 3 的 Composition API 中,侦听器是响应式编程的核心部分之一。它允许我们观察和响应响应式数据的变化,执行副作用(side effects)或异步操作。Vue 提供了两个主要的侦听器函数:watchwatchEffect

侦听器的基本概念

侦听器(Watchers)用于观察响应式数据的变化,并在变化发生时执行特定的逻辑。这在以下场景中特别有用:

  • 执行异步操作(如API调用)
  • 在数据变化时执行复杂的业务逻辑
  • 监听表单输入的变化并触发验证
  • 响应路由变化
  • 执行数据持久化(如保存到本地存储)
Composition API 优势

Composition API 提供了更灵活和强大的侦听器功能,相比 Options API 的 watch 选项,它在处理复杂逻辑和重用侦听器逻辑方面更加强大。

watch() 函数

watch() 函数用于显式地侦听一个或多个响应式数据源,并在它们变化时执行回调函数。

基本语法

语法watch 函数的基本用法

import { ref, watch } from 'vue'

// 语法:watch(source, callback, options?)
const count = ref(0)

// 侦听单个数据源
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})

// 触发变化
count.value++ // 输出: count 从 0 变为 1
                        

参数说明

参数 类型 说明
source Getter 函数 / Ref / Reactive 对象 / 数组 要侦听的数据源。可以是:
1. 一个返回值的 getter 函数
2. 一个 ref
3. 一个 reactive 对象
4. 包含以上类型的数组
callback 函数 数据变化时执行的回调函数。接收新值和旧值作为参数。
options 对象(可选) 配置选项,包括:
immediatedeepflushonTrackonTrigger

不同的数据源类型

1. 侦听 ref

import { ref, watch } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 侦听 ref
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

// 也可以使用 getter 函数
watch(() => count.value, (newVal, oldVal) => {
  console.log(`count via getter: ${oldVal} → ${newVal}`)
})
                        
2. 侦听 reactive 对象

import { reactive, watch } from 'vue'

const state = reactive({
  count: 0,
  user: {
    name: '张三',
    age: 25
  }
})

// 侦听整个 reactive 对象(需要 deep: true)
watch(
  () => state,
  (newVal, oldVal) => {
    console.log('state 变化了:', newVal)
  },
  { deep: true }
)

// 侦听 reactive 对象的特定属性(推荐)
watch(
  () => state.count,
  (newVal, oldVal) => {
    console.log(`state.count: ${oldVal} → ${newVal}`)
  }
)

// 侦听嵌套属性
watch(
  () => state.user.name,
  (newName, oldName) => {
    console.log(`用户名: ${oldName} → ${newName}`)
  }
)
                        
3. 侦听多个数据源

import { ref, watch } from 'vue'

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

// 侦听多个 ref
watch([firstName, lastName, age], (newValues, oldValues) => {
  const [newFirst, newLast, newAge] = newValues
  const [oldFirst, oldLast, oldAge] = oldValues
  console.log(`变化: ${oldFirst}${oldLast}(${oldAge}) → ${newFirst}${newLast}(${newAge})`)
})

// 侦听混合数据源
watch(
  [() => firstName.value, () => age.value],
  ([newFirst, newAge], [oldFirst, oldAge]) => {
    console.log(`firstName: ${oldFirst} → ${newFirst}, age: ${oldAge} → ${newAge}`)
  }
)
                        

配置选项

immediate: 立即执行

import { ref, watch } from 'vue'

const count = ref(0)

// 设置 immediate: true,侦听器会立即执行一次
watch(
  count,
  (newVal, oldVal) => {
    console.log(`count: ${oldVal} → ${newVal}`)
  },
  { immediate: true }
)
// 立即输出: count: undefined → 0

count.value = 1 // 输出: count: 0 → 1
                        
deep: 深度侦听

import { reactive, watch } from 'vue'

const state = reactive({
  user: {
    name: '张三',
    details: {
      age: 25,
      email: 'zhangsan@example.com'
    }
  }
})

// 深度侦听整个对象
watch(
  () => state.user,
  (newVal, oldVal) => {
    console.log('user 对象发生变化:', newVal)
  },
  { deep: true }
)

// 修改嵌套属性会触发侦听器
state.user.details.age = 26 // 会触发侦听器
state.user.name = '李四' // 会触发侦听器
                        
注意:深度侦听的性能

深度侦听(deep: true)会遍历对象的所有嵌套属性,这可能会在大型对象上产生性能开销。只在实际需要时使用深度侦听。

flush: 执行时机

import { ref, watch } from 'vue'

const count = ref(0)

// flush: 'pre' - 默认值,在组件更新前执行
watch(
  count,
  () => {
    console.log('pre: 在组件更新前执行')
  },
  { flush: 'pre' }
)

// flush: 'post' - 在组件更新后执行
watch(
  count,
  () => {
    console.log('post: 在组件更新后执行')
  },
  { flush: 'post' }
)

// flush: 'sync' - 同步执行,在依赖变化时立即执行
watch(
  count,
  () => {
    console.log('sync: 立即执行')
  },
  { flush: 'sync' }
)
                        

watchEffect() 函数

watchEffect() 函数会自动追踪其回调函数中使用的响应式依赖,并在这些依赖变化时重新执行回调。它不需要显式指定要侦听的数据源。

自动依赖追踪

watchEffect 会自动追踪回调函数中访问的所有响应式属性,无需手动指定要侦听的数据源。

基本语法

语法watchEffect 的基本用法

import { ref, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// watchEffect 会自动追踪 count 和 message
watchEffect(() => {
  console.log(`count: ${count.value}, message: ${message.value}`)
  // 这里访问了 count.value 和 message.value,
  // 所以当它们中任何一个变化时,都会重新执行这个函数
})

// 触发重新执行
count.value++ // 输出: count: 1, message: Hello
message.value = 'Hi' // 输出: count: 1, message: Hi
                        

watch vs watchEffect

特性 watch watchEffect
数据源 需要显式指定要侦听的数据源 自动追踪回调函数中访问的响应式依赖
初始执行 默认不立即执行(可通过 immediate: true 启用) 立即执行一次
新旧值 可以访问旧值和新值 不能访问旧值,只能访问当前值
使用场景 需要精确控制侦听的数据源时
需要访问旧值时
自动依赖追踪更方便时
不需要旧值时
性能 更精确控制,可能更高效 自动依赖追踪可能包含不需要的依赖

实际应用示例

示例1:搜索框防抖

示例使用 watch 实现搜索防抖

import { ref, watch } from 'vue'

// 搜索关键词
const searchQuery = ref('')
// 搜索结果
const searchResults = ref([])
// 加载状态
const isLoading = ref(false)
// 防抖计时器
let debounceTimer = null

// 侦听搜索关键词的变化
watch(searchQuery, (newQuery) => {
  // 清除之前的计时器
  clearTimeout(debounceTimer)

  // 如果搜索词为空,清空结果
  if (!newQuery.trim()) {
    searchResults.value = []
    return
  }

  // 设置防抖计时器
  debounceTimer = setTimeout(async () => {
    isLoading.value = true
    try {
      // 模拟 API 调用
      const response = await fetch(
        `https://api.example.com/search?q=${encodeURIComponent(newQuery)}`
      )
      const data = await response.json()
      searchResults.value = data.results
    } catch (error) {
      console.error('搜索失败:', error)
      searchResults.value = []
    } finally {
      isLoading.value = false
    }
  }, 500) // 500ms 防抖延迟
})

// 当用户输入时自动触发搜索
// searchQuery.value = 'Vue.js' // 会触发防抖搜索
                        

示例2:表单自动保存

示例使用 watchEffect 实现表单自动保存

import { reactive, watchEffect } from 'vue'

// 表单数据
const formData = reactive({
  title: '',
  content: '',
  tags: [],
  lastSaved: null
})

// 自动保存的防抖计时器
let saveTimer = null

// 使用 watchEffect 自动追踪表单数据变化
watchEffect((onCleanup) => {
  // 访问表单数据,自动追踪依赖
  const { title, content, tags } = formData

  // 清理之前的计时器
  onCleanup(() => {
    clearTimeout(saveTimer)
  })

  // 设置防抖保存
  saveTimer = setTimeout(async () => {
    try {
      // 模拟保存到服务器
      await fetch('/api/save-form', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ title, content, tags })
      })

      // 更新最后保存时间
      formData.lastSaved = new Date().toLocaleTimeString()
      console.log('表单已自动保存')
    } catch (error) {
      console.error('保存失败:', error)
    }
  }, 1000) // 1秒防抖
})

// 当用户修改表单时,会自动触发保存
// formData.title = '新标题' // 会触发自动保存
                        

示例3:路由参数监听

示例监听路由参数变化

import { watch, ref } from 'vue'
import { useRoute } from 'vue-router'

// 获取当前路由
const route = useRoute()
const userId = ref(null)
const userData = ref(null)

// 监听路由参数变化
watch(
  () => route.params.id,
  async (newId) => {
    if (!newId) return

    userId.value = newId

    try {
      // 根据新的用户ID加载用户数据
      const response = await fetch(`/api/users/${newId}`)
      userData.value = await response.json()
    } catch (error) {
      console.error('加载用户数据失败:', error)
      userData.value = null
    }
  },
  { immediate: true } // 组件创建时立即执行一次
)

// 监听查询参数变化
watch(
  () => route.query.page,
  (newPage) => {
    console.log('页码变化:', newPage)
    // 重新加载列表数据
    loadData(newPage || 1)
  }
)
                        

停止侦听器

watchwatchEffect 都会返回一个停止函数,调用这个函数可以停止侦听。

示例手动停止侦听器

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

const count = ref(0)

// watch 返回停止函数
const stopWatch = watch(count, (newVal) => {
  console.log('watch:', newVal)
  if (newVal >= 5) {
    console.log('达到5,停止watch')
    stopWatch() // 停止侦听
  }
})

// watchEffect 也返回停止函数
const stopEffect = watchEffect(() => {
  console.log('watchEffect:', count.value)
  if (count.value >= 3) {
    console.log('达到3,停止watchEffect')
    stopEffect() // 停止侦听
  }
})

// 在组件卸载时停止所有侦听器
import { onUnmounted } from 'vue'
onUnmounted(() => {
  stopWatch()
  stopEffect()
})
                        

清理副作用

watchEffect 的回调函数接收一个 onCleanup 参数,用于注册清理函数。当侦听器重新执行或停止时,清理函数会被调用。

示例使用 onCleanup 清理副作用

import { ref, watchEffect } from 'vue'

const searchQuery = ref('')

watchEffect((onCleanup) => {
  // 创建 AbortController 用于取消请求
  const controller = new AbortController()
  const signal = controller.signal

  // 注册清理函数
  onCleanup(() => {
    console.log('清理之前的请求')
    controller.abort() // 取消请求
  })

  // 执行异步操作
  if (searchQuery.value.trim()) {
    fetch(`/api/search?q=${searchQuery.value}`, { signal })
      .then(response => response.json())
      .then(data => {
        console.log('搜索结果:', data)
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          console.error('搜索失败:', err)
        }
      })
  }
})

// 当 searchQuery 快速变化时,
// 之前的请求会被自动取消,避免竞态条件
                        

高级用法

异步侦听器

示例异步侦听器

import { ref, watch } from 'vue'

const userId = ref(1)

// 异步侦听器
watch(
  userId,
  async (newId, oldId, onCleanup) => {
    let cancelled = false

    // 注册清理函数
    onCleanup(() => {
      cancelled = true
    })

    try {
      // 模拟异步数据获取
      const response = await fetch(`/api/user/${newId}`)
      const userData = await response.json()

      // 检查是否被取消
      if (!cancelled) {
        console.log('用户数据:', userData)
        // 更新组件状态...
      }
    } catch (error) {
      if (!cancelled) {
        console.error('加载用户数据失败:', error)
      }
    }
  },
  { immediate: true }
)
                        

调试选项

示例使用 onTrack 和 onTrigger 调试

import { ref, watch } from 'vue'

const count = ref(0)
const double = ref(0)

watch(
  count,
  (newVal) => {
    double.value = newVal * 2
  },
  {
    onTrack(e) {
      console.log('依赖被追踪:', e)
    },
    onTrigger(e) {
      console.log('依赖变化触发:', e)
    }
  }
)

// 修改 count 会触发 onTrack 和 onTrigger
count.value = 5
                        

最佳实践

侦听器最佳实践
  • 精确侦听:尽量只侦听需要的数据,而不是整个对象
  • 避免深度侦听:只在必要时使用 deep: true,因为它有性能开销
  • 清理副作用:使用 onCleanup 清理异步操作,避免内存泄漏
  • 防抖处理:对频繁变化的数据使用防抖,避免过多请求
  • 停止侦听器:在组件卸载时停止不再需要的侦听器
  • 合理选择:根据需求选择 watchwatchEffect
常见陷阱
  • 无限循环:避免在侦听器中修改正在侦听的数据
  • 异步竞争:快速变化的数据可能导致异步操作竞争,使用 AbortController 解决
  • 性能问题:侦听大型对象或频繁变化的数据可能导致性能问题
  • 内存泄漏:忘记停止侦听器可能导致内存泄漏

实时演示

侦听器演示

尝试修改以下值,观察控制台输出(打开浏览器开发者工具查看)

25
等待变化...

总结

Vue 3 Composition API 中的侦听器提供了强大而灵活的方式来响应数据变化。通过 watchwatchEffect,我们可以:

  • 精确控制要侦听的数据源
  • 自动追踪响应式依赖
  • 执行异步操作和副作用
  • 清理资源避免内存泄漏
  • 调试和优化性能

理解 watchwatchEffect 的区别及适用场景,可以帮助我们编写更高效、更易维护的 Vue 应用。记住始终遵循最佳实践,避免常见陷阱。

测试你的理解

思考题
  1. watchwatchEffect 的主要区别是什么?
  2. 什么时候应该使用 deep: true 选项?
  3. 如何避免在侦听器中产生无限循环?
  4. 为什么需要在 watchEffect 中使用 onCleanup
  5. 如何处理快速变化的数据导致的异步竞争问题?