vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3 watch与watchEffect详解

Vue3中watch与watchEffect使用方法详解

作者:时间sk

在Vue中,watch和watchEffect都是用于观察和响应数据变化的工具,但它们在使用方式和功能上有一些显著的区别,这篇文章主要介绍了Vue3中watch与watchEffect使用的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

一、监听器的基本概念

在Vue3中,watchwatchEffect都是用于监听数据变化并执行副作用的API,但它们的使用方式和适用场景有所不同。

二、watch的使用方法

2.1 基础用法:监听单个ref数据

<template>
  <div>
    <p>计数器: {{ count }}</p>
    <button @click="count++">增加</button>
  </div>
</template>

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

// 创建响应式数据
const count = ref(0)

// 监听count变化
// 第一个参数:要监听的数据源
// 第二个参数:回调函数,接收新值和旧值
watch(count, (newValue, oldValue) => {
  console.log(`count从${oldValue}变为${newValue}`)
})
</script>

2.2 监听多个数据源【监听多个数据,用数组形式传入】

import { ref, watch } from 'vue'

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

// 监听多个数据,用数组形式传入
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从${oldFirst}${oldLast}变为${newFirst}${newLast}`)
})

2.3 监听对象属性【监听对象的某个属性,使用getter函数】

import { reactive, watch } from 'vue'

const user = reactive({
  name: '张三',
  age: 20
})

// 监听对象的某个属性,使用getter函数
watch(
  () => user.age,  // getter函数,返回要监听的属性
  (newAge, oldAge) => { console.log(`年龄从${oldAge}变为${newAge}`)
  }
)

2.4 深度监听

当监听对象或数组时,默认情况下watch不会监听内部属性变化,需要使用deep: true开启深度监听:

import { ref, watch } from 'vue'

const user = ref({
  name: '张三',
  address: {
    city: '北京'
  }
})

// 深度监听对象内部变化
watch(
  user,
  (newVal, oldVal) => { console.log('用户信息变化:', newVal)},
  { deep: true }  // 开启深度监听
)

// 修改深层属性会触发监听
user.value.address.city = '上海'

2.5 立即执行

默认情况下,watch是惰性的,只有数据变化时才执行。使用immediate: true可以让它在初始化时立即执行

watch(
  count,
  (newValue, oldValue) => {
    console.log(`count变化: ${newValue}`)
  },
  { immediate: true }  // 立即执行
)

三、watchEffect的使用方法【可以不用指定监听的数据】

3.1 基础用法

watchEffect会自动收集函数内使用的响应式数据作为依赖:

自动追踪所有响应式依赖, 每次依赖变更都会执行此函数

<template>
  <div>
    <p>计数器: {{ count }}</p>
    <button @click="count++">增加</button>
  </div>
</template>

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

const count = ref(0)

// watchEffect会自动收集依赖
// 1. 初始化时立即执行一次
// 2. 当函数内依赖的响应式数据变化时重新执行
watchEffect(  () => {console.log(`count的值是: ${count.value}`)}
)
</script>

3.2 停止监听

watchEffect返回一个停止函数,调用它可以停止监听:

import { ref, watchEffect } from 'vue'

const count = ref(0)

// 获取停止函数
const stop = watchEffect(() => {
  console.log(`count: ${count.value}`)
})

// 5秒后停止监听
setTimeout(() => {
  stop()
  console.log('已停止监听')
}, 5000)

3.3 清理副作用

watchEffect的回调函数可以返回一个清理函数,在下次执行前或停止监听时调用:

watchEffect((onInvalidate) => {
  // 模拟异步操作
  const timer = setTimeout(() => {
    console.log('异步操作完成')
  }, 1000)
  
  // 清理函数,在下次执行前或停止监听时调用
  onInvalidate(() => {
    clearTimeout(timer)
    console.log('清理定时器')
  })
})

四、watch与watchEffect的区别

特性watchwatchEffect
依赖收集需要显式指定监听源自动收集函数内的响应式依赖
执行时机默认惰性执行(数据变化时)立即执行,然后响应式追踪
新旧值可以获取新值和旧值只能获取新值
适用场景需要知道数据变化前后的值只需响应数据变化,不需要旧值
控制粒度精确控制监听源自动追踪所有依赖

五、高级用法

5.1 监听执行时机控制

使用flush选项控制回调执行时机:

watch(
  count,
  () => {
    // DOM更新后执行
  },
  { flush: 'post' }  // 'pre'(默认) | 'post' | 'sync'
)

5.2 调试监听器

使用onTrackonTrigger选项调试依赖:

watch(
  count,
  () => {
    console.log('count变化了')
  },
  {
    onTrack(e) {
      console.log('依赖被追踪:', e)
    },
    onTrigger(e) {
      console.log('监听器被触发:', e)
    }
  }
)

5.3 暂停与恢复监听(Vue3.5+)

Vue3.5+版本新增了暂停和恢复监听的功能:

const { pause, resume } = watch(count, () => {
  console.log('count变化了')
})

// 暂停监听
pause()

// 恢复监听
resume()

六、实战示例

6.1 表单验证(使用watch)

import { ref, watch } from 'vue'

const username = ref('')
const errorMessage = ref('')

// 监听用户名变化进行验证
watch(
  username,
  (newVal) => {
    if (newVal.length < 3) {
      errorMessage.value = '用户名至少3个字符'
    } else {
      errorMessage.value = ''
    }
  },
  { immediate: true }  // 初始化时验证
)

6.2 数据加载与清理(使用watchEffect)

import { ref, watchEffect } from 'vue'

const userId = ref(1)
const userData = ref(null)

watchEffect(async (onInvalidate) => {
  // 加载数据
  const controller = new AbortController()
  const signal = controller.signal
  
  try {
    const response = await fetch(`/api/user/${userId.value}`, { signal })
    userData.value = await response.json()
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('加载失败:', error)
    }
  }
  
  // 清理函数,取消上一次请求
  onInvalidate(() => {
    controller.abort()
  })
})

七、总结

7.1 watch监听reactive对象的注意事项

当监听reactive创建的响应式对象时,有一些特殊情况需要注意:

import { reactive, watch } from 'vue'

const user = reactive({
  name: '张三',
  age: 20
})

// 情况一:直接监听reactive对象
// 此时会自动开启深度监听,无需设置deep: true
watch(user, (newVal, oldVal) => {
  console.log('用户信息变化', newVal)
})

// 情况二:监听reactive对象的属性
// 必须使用getter函数,否则无法触发监听
watch(
  () => user.age,  // 正确写法
  (newAge) => {
    console.log('年龄变化:', newAge)
  }
)

// 错误写法:直接监听属性无法触发
watch(user.age, () => {
  console.log('年龄变化') // 不会执行
})

7.2 watchEffect的依赖追踪细节

watchEffect的依赖追踪是动态的,只追踪函数执行过程中实际访问的响应式数据:

import { ref, watchEffect } from 'vue'

const a = ref(0)
const b = ref(0)

watchEffect(() => {
  console.log('watchEffect执行')
  if (a.value > 5) {
    console.log('b的值:', b.value)
  }
})

// 初始执行: 输出 "watchEffect执行"

a.value = 3  // 执行: 输出 "watchEffect执行" (a被访问)
b.value = 5  // 不执行 (b未被访问)
a.value = 6  // 执行: 输出 "watchEffect执行" 和 "b的值: 5" (a和b都被访问)
b.value = 10 // 执行: 输出 "watchEffect执行" 和 "b的值: 10" (b现在被访问了)

7.3 高级调试技巧

使用onTrackonTrigger选项可以帮助我们调试监听器的行为:

watch(
  count,
  () => {
    console.log('count变化了')
  },
  {
    onTrack(e) {
      // 当依赖被追踪时触发
      console.log(`追踪到依赖: ${e.target}的${e.key}属性`)
    },
    onTrigger(e) {
      // 当监听器被触发时触发
      console.log(`监听器触发原因: ${e.type}`)
    }
  }
)

7.4 性能优化策略

7.4.1 避免不必要的深度监听

深度监听会递归遍历对象的所有属性,对于大型对象会影响性能:

// 不推荐:深度监听大型对象
watch(largeObject, () => {
  // ...
}, { deep: true })

// 推荐:只监听需要的属性
watch(
  () => largeObject.importantProperty,
  () => {
    // ...
  }
)

7.4.2 使用防抖/节流优化频繁触发

对于输入框等频繁变化的场景,可以结合防抖/节流:

import { ref, watchEffect } from 'vue'
import { debounce } from 'lodash'

const searchInput = ref('')

// 使用防抖优化搜索请求
const debouncedSearch = debounce(async (value) => {
  const results = await fetch(`/api/search?q=${value}`)
  // 处理结果
}, 300)

watchEffect(() => {
  debouncedSearch(searchInput.value)
})

八、常见问题与解决方案

8.1 监听器不触发的常见原因

  1. 监听了非响应式数据
// 错误:监听普通变量
let count = 0
watch(count, () => { /* 不会触发 */ })

// 正确:监听响应式数据
const count = ref(0)
watch(count, () => { /* 会触发 */ })
  1. 直接修改数组索引或长度
const list = ref([1, 2, 3])

// 错误:直接修改索引
list.value[0] = 100  // 不会触发监听

// 正确:使用数组方法或set
list.value.push(4)   // 会触发
list.value.splice(0, 1, 100)  // 会触发
  1. 监听对象新增属性
const user = reactive({ name: '张三' })

// 错误:直接添加属性
user.age = 20  // 默认不会触发监听

// 正确:使用Vue.set或重新赋值
user.age = 20  // 在Vue3中,reactive对象新增属性会触发监听
// 或对于ref对象
user.value = { ...user.value, age: 20 }

8.2 watch与computed的选择

场景推荐使用原因
数据转换/计算computed缓存结果,更高效
数据变化时执行异步操作watch适合处理副作用
需要知道数据变化前后的值watchcomputed无法获取旧值
简单的依赖追踪watchEffect代码更简洁

九、完整实战案例:搜索功能实现

下面是一个结合watch、watchEffect和computed的完整搜索功能实现:

<template>
  <div class="search-container">
    <input 
      v-model="searchQuery" 
      placeholder="搜索..."
      @input="handleInput"
    >
    <div v-if="isSearching" class="loading">搜索中...</div>
    <div v-if="errorMessage" class="error">{{ errorMessage }}</div>
    <ul v-else-if="results.length">
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
    <div v-else-if="!isSearching && searchQuery">无结果</div>
  </div>
</template>

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

// 响应式数据
const searchQuery = ref('')
const results = ref([])
const isSearching = ref(false)
const errorMessage = ref('')
const debouncedQuery = ref('')

// 防抖处理
let timeoutId = null
const handleInput = () => {
  clearTimeout(timeoutId)
  timeoutId = setTimeout(() => {
    debouncedQuery.value = searchQuery.value
  }, 300)
}

// 使用watch处理搜索逻辑
watch(
  debouncedQuery,
  async (newQuery) => {
    if (!newQuery.trim()) {
      results.value = []
      return
    }
    
    isSearching.value = true
    errorMessage.value = ''
    
    try {
      const response = await fetch(`/api/search?q=${encodeURIComponent(newQuery)}`)
      if (!response.ok) throw new Error('搜索失败')
      results.value = await response.json()
    } catch (err) {
      errorMessage.value = err.message
    } finally {
      isSearching.value = false
    }
  },
  { immediate: false }
)

// 使用watchEffect清理定时器
watchEffect((onInvalidate) => {
  // 组件卸载时清理定时器
  onInvalidate(() => {
    clearTimeout(timeoutId)
  })
})

// 清理函数
onUnmounted(() => {
  clearTimeout(timeoutId)
})
</script>

十、总结与最佳实践

  1. 优先使用watchEffect:当你需要自动追踪多个依赖,且不需要旧值时
  2. 使用watch的场景:需要明确监听源、需要旧值、需要延迟执行或深度监听
  3. 性能考量
    • 避免对大型对象使用深度监听
    • 对频繁变化的数据使用防抖/节流
    • 及时清理副作用,避免内存泄漏
  4. 调试技巧:使用onTrack和onTrigger追踪依赖问题
  5. 组件卸载:异步创建的监听器需要手动停止

通过合理选择watch和watchEffect,并遵循最佳实践,可以写出更高效、更可维护的Vue3代码。理解它们的内部工作原理和适用场景,将帮助你在不同的业务需求中做出正确的技术选择。## 十二、底层原理与实现机制

10.1 响应式依赖收集流程

Vue3的监听器基于响应式系统工作,其核心流程如下:

1. 当watch/watchEffect执行时,会创建一个"副作用函数"(effect)
2. 执行副作用函数,期间访问的响应式数据会被"追踪"(track)
3. 响应式数据变化时,会"触发"(trigger)相关的副作用函数重新执行

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  响应式数据  │────▶│  依赖追踪器  │────▶│ 副作用函数队列 │
└─────────────┘     └─────────────┘     └─────────────┘
       ▲                                          │
       │                                          ▼
       └──────────────────────────────────────────┘
              (数据变化时触发副作用执行)

10.2 watch与watchEffect的内部差异

// watch的简化实现
function watch(source, callback, options) {
  // 创建getter函数
  const getter = isRef(source) 
    ? () => source.value 
    : typeof source === 'function' 
      ? source 
      : () => traverse(source);
  
  // 执行副作用
  let oldValue = undefined;
  const effect = effectFn(() => getter(), {
    lazy: true, // 懒执行,首次不触发
    scheduler: () => { // 调度函数
      const newValue = getter();
      callback(newValue, oldValue);
      oldValue = newValue;
    }
  });
  
  // 初始化
  if (options.immediate) {
    callback(getter(), oldValue);
  } else {
    oldValue = effect();
  }
  
  return () => stop(effect);
}

// watchEffect的简化实现
function watchEffect(effect, options) {
  const _effect = effectFn(effect, {
    lazy: false, // 立即执行
    scheduler: queueJob, // 默认调度器
    ...options
  });
  
  return () => stop(_effect);
}

十一、API参数全解析

11.1 watch完整参数说明

// TypeScript类型定义
function watch<T>(
  source: WatchSource<T> | WatchSource<T>[],
  callback: WatchCallback<T>,
  options?: WatchOptions
): StopHandle

interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean  // 是否立即执行,默认false
  deep?: boolean       // 是否深度监听,默认false
  flush?: 'pre' | 'post' | 'sync'  // 执行时机,默认'pre'
  once?: boolean       // 是否只执行一次,Vue3.4+,默认false
  onTrack?: (event: DebuggerEvent) => void  // 依赖追踪时触发
  onTrigger?: (event: DebuggerEvent) => void  // 触发更新时触发
}

11.2 flush选项详解

取值执行时机适用场景
‘pre’组件更新前执行需要访问更新前的DOM
‘post’组件更新后执行需要访问更新后的DOM
‘sync’同步执行需要立即响应数据变化
// 示例:在DOM更新后执行
watch(
  count,
  () => {
    // 此时可以获取到更新后的DOM
    console.log('DOM高度:', document.getElementById('content').offsetHeight)
  },
  { flush: 'post' }
)

十二、TypeScript高级用法

12.1 类型定义与推断

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

// 1. 监听ref
const count = ref<number>(0)
watch<number>(count, (newVal, oldVal) => {
  // newVal和oldVal都会被推断为number类型
})

// 2. 监听reactive对象
interface User {
  name: string
  age: number
}
const user = reactive<User>({ name: '张三', age: 20 })
watch(
  () => user.age, 
  (newAge: number, oldAge: number) => {
    // 显式指定类型
  }
)

// 3. 监听多个源
watch<[number, string]>(
  [count, () => user.name], 
  ([newCount, newName], [oldCount, oldName]) => {
    // 类型安全
  }
)

12.2 自定义监听器类型

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

// 定义通用监听器类型
type CustomWatcher<T> = (
  source: WatchSource<T>,
  callback: (newVal: T, oldVal: T) => void
) => void

// 创建带类型的监听器
const numberWatcher: CustomWatcher<number> = (source, callback) => {
  return watch(source, callback)
}

// 使用
const count = ref(0)
numberWatcher(count, (newVal, oldVal) => {
  // 类型安全
})

十三、调试与问题诊断

13.1 使用Vue DevTools调试

Vue DevTools提供了专门的监听器调试面板:

  1. 查看监听器列表:在"Components"面板中选择组件,查看"Watchers"部分
  2. 触发时机分析:使用"Timeline"面板记录监听器触发时间线
  3. 依赖可视化:查看每个监听器依赖的响应式数据

13.2 常见问题诊断流程

监听器不触发时的排查步骤:

1. 确认监听源是响应式数据
   - 使用isRef/isReactive检查
   - 确认不是解构后的值

2. 检查监听路径是否正确
   - 对象属性需使用getter函数
   - 数组需监听整个数组或使用正确索引

3. 验证数据确实发生了变化
   - 使用console.log打印数据
   - 确认不是引用类型数据的内部变化

4. 检查是否需要深度监听
   - 对象/数组内部变化需设置deep: true

5. 检查是否有条件执行问题
   - watchEffect中是否有条件语句导致依赖未被访问

十四、性能优化高级技巧

14.1 精确监听避免过度触发

// 不好的做法:监听整个对象
watch(
  user,
  () => {
    // 即使无关属性变化也会触发
  },
  { deep: true }
)

// 好的做法:只监听需要的属性
watch(
  () => ({ name: user.name, age: user.age }),
  ({ name, age }) => {
    // 只有这两个属性变化才触发
  }
)

14.2 监听器防抖与节流

import { ref, watch } from 'vue'
import { debounce, throttle } from 'lodash'

// 防抖示例
const searchInput = ref('')
const debouncedSearch = debounce((value) => {
  // 搜索逻辑
}, 300)

watch(searchInput, debouncedSearch)

// 节流示例
const scrollPosition = ref(0)
const throttledScroll = throttle((position) => {
  // 滚动处理逻辑
}, 100)

watch(scrollPosition, throttledScroll)

14.3 大数据场景优化

对于大型列表或复杂对象,使用shallowRefshallowReactive减少响应式开销:

import { shallowRef, watch } from 'vue'

// 大型数据使用shallowRef
const largeList = shallowRef([])

// 只监听引用变化,不监听内部属性
watch(largeList, () => {
  // 只有当整个列表被替换时才触发
})

十五、Vue2迁移指南

15.1 watch选项式API与组合式API对比

Vue2选项式APIVue3组合式API
javascript watch: { count(newVal, oldVal) { // 处理逻辑 } } javascript watch(count, (newVal, oldVal) => { // 处理逻辑 })
javascript watch: { 'user.name'(newVal) { // 监听嵌套属性 } } javascript watch(() => user.name, (newVal) => { // 监听嵌套属性 })
javascript watch: { user: { handler: () => {}, deep: true } } javascript watch(user, () => {}, { deep: true })

15.2 迁移注意事项

  1. this绑定:Vue3组合式API中没有this,直接使用响应式变量
  2. 深度监听:Vue3中reactive对象默认深度监听,ref对象需要设置deep: true
  3. 数组监听:Vue3中直接修改数组索引可以触发监听(得益于Proxy)
  4. **watch方法∗∗:实例方法‘this.watch方法**:实例方法`this.watch方法:实例方法this.watch替换为watch`函数

十六、单元测试示例

使用Jest测试监听器行为:

import { ref, watch, watchEffect } from 'vue'
import { mount } from '@vue/test-utils'

describe('watch测试', () => {
  it('基本监听功能', async () => {
    const count = ref(0)
    const mockCallback = jest.fn()
    
    watch(count, mockCallback)
    
    // 初始状态不触发
    expect(mockCallback).not.toHaveBeenCalled()
    
    // 修改值后触发
    count.value = 1
    await Promise.resolve() // 等待微任务完成
    
    expect(mockCallback).toHaveBeenCalledWith(1, 0)
  })
  
  it('watchEffect立即执行', () => {
    const mockEffect = jest.fn()
    watchEffect(mockEffect)
    
    // 立即执行
    expect(mockEffect).toHaveBeenCalled()
  })
})

十七、总结:监听器选择决策指南

面对不同场景,如何选择合适的监听器API:

┌─────────────────────────────────────────────┐
│  需要知道数据变化前后的值                   │
│  ↓                                         │
│  需要明确指定监听源                         │
│  ↓              ┌───────────────┐          │
│  是────────────▶│     watch     │◀─────────┐
│  ↓              └───────────────┘          │
│  否                                         │
│  ↓              ┌───────────────┐          │
│  需要自动收集依赖 ─────────────▶│watchEffect│◀─────────┐
│                 └───────────────┘          │
│                                            │
│  需要缓存计算结果 ─────────────▶│ computed  │
└─────────────────────────────────────────────┘

最终建议

通过这一章节的补充,您现在应该对Vue3的监听器系统有了全面且深入的理解,能够在各种场景下选择合适的API并编写高效、可维护的代码。

到此这篇关于Vue3中watch与watchEffect使用方法的文章就介绍到这了,更多相关Vue3 watch与watchEffect详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文