前端摸鱼匠:个人主页

个人专栏:《vue3入门到精通

没有好的理念,只有脚踏实地!

一、初识watchEffect:响应式编程的利器

1.1 什么是watchEffect

在Vue3的Composition API中,watchEffect是一个极其强大的响应式API,它为我们提供了一种自动追踪依赖并执行副作用的方式。根据官方文档的定义,watchEffect会立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行【turn0search1】。这意味着我们不需要显式地指定要监听的数据源,watchEffect会自动检测函数内部使用的响应式数据,并在这些数据变化时重新运行函数。

import { ref, watchEffect } from 'vue'
const count = ref(0)
// 立即执行,并自动追踪count.value作为依赖
watchEffect(() => {
console.log(`计数器的值是: ${count.value}`)
})

1.2 watchEffect的核心特点

watchEffect具有几个显著特点,使其在许多场景下比传统的watch更加便捷:

  • 自动依赖收集:不需要手动指定监听源,函数内使用的响应式数据都会被自动追踪【turn0search1】
  • 立即执行:创建时会立即执行一次,用于建立初始依赖关系【turn0search4】
  • 简洁性:代码更加简洁,减少了显式声明依赖的需要【turn0search2】
  • 响应式追踪:底层使用Vue的响应式系统,高效追踪依赖变化【turn0search16】

1.3 与watch的初步对比

虽然watchEffect和watch都能监听数据变化,但它们在设计理念上有明显区别:

特性watchEffectwatch
依赖追踪自动收集手动指定
懒执行否(立即执行)是(默认)
获取新旧值
使用场景依赖关系复杂需要精确控制

二、watchEffect的基本用法

2.1 基础语法结构

watchEffect的基本语法非常简洁,接受两个参数:一个副作用函数和一个可选的配置对象【turn0search3】。

// 基本语法
watchEffect(
() => {
// 副作用函数内容
},
{
// 可选配置项
flush: 'pre', // 'pre' | 'post' | 'sync'
onTrack: (e) => {}, // 调试钩子
onTrigger: (e) => {} // 调试钩子
}
)

2.2 监听ref类型数据

当监听ref定义的基本类型数据时,watchEffect会自动追踪其value属性的变化【turn0search1】。


<script setup>
import { ref, watchEffect } from 'vue'
// 定义ref响应式数据
const count = ref(0)
// 监听ref数据
watchEffect(() => {
  console.log(`count的值变化了: ${count.value}`)
  // 这里会自动追踪count.value作为依赖
})
const increment = () => {
  count.value++
}
</script>

2.3 监听reactive类型数据

对于reactive定义的对象,watchEffect可以深度追踪其内部属性的变化【turn0search1】。


<script setup>
import { reactive, watchEffect } from 'vue'
// 定义reactive响应式对象
const user = reactive({
  name: '张三',
  age: 25,
  address: {
    city: '北京'
  }
})
// 监听reactive对象
watchEffect(() => {
  console.log(`用户信息: ${user.name}, ${user.age}, ${user.address.city}`)
  // 自动追踪user对象及其嵌套属性的变化
})
const updateUser = () => {
  user.name = '李四'
  user.age = 30
  user.address.city = '上海'
}
</script>

2.4 监听多个数据源

watchEffect可以同时监听多个响应式数据,无需特殊处理,只要在函数中使用这些数据即可【turn0search2】。

import { ref, reactive, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
const user = reactive({ name: 'Vue', version: 3 })
// 同时监听多个数据源
watchEffect(() => {
console.log(`计数: ${count.value}, 消息: ${message.value}, 用户: ${user.name} v${user.version}`)
// 自动追踪所有使用的响应式数据
})

三、watchEffect的配置选项

3.1 flush选项:控制执行时机

flush选项用于控制副作用函数的触发时机,有三个可选值:‘pre’(默认)、‘post’和’sync’【turn0search1】。

3.1.1 flush: ‘pre’(默认值)

默认情况下,watchEffect会在组件更新之前执行副作用函数【turn0search1】。

import { ref, watchEffect } from 'vue'
const count = ref(0)
// 默认flush: 'pre',在组件更新前执行
watchEffect(() => {
console.log(`pre - count的值: ${count.value}`)
// 此时DOM还未更新
})
count.value++
// 输出顺序: pre - count的值: 1 -> 组件更新
3.1.2 flush: ‘post’

将flush设置为’post’可以使副作用函数在组件更新后执行,这对于需要访问更新后的DOM元素的场景非常有用【turn0search1】。

import { ref, watchEffect, onMounted } from 'vue'
const count = ref(0)
const elementRef = ref(null)
// flush: 'post',在组件更新后执行
watchEffect(
() => {
console.log(`post - count的值: ${count.value}`)
// 此时DOM已更新,可以访问更新后的DOM
if (elementRef.value) {
console.log('DOM元素高度:', elementRef.value.clientHeight)
}
},
{ flush: 'post' }
)
count.value++
// 输出顺序: 组件更新 -> post - count的值: 1
3.1.3 flush: ‘sync’

将flush设置为’sync’可以使副作用同步触发,而不是等到下一个微任务队列【turn0search1】。这意味着副作用会立即在响应式数据变化时执行。

import { ref, watchEffect } from 'vue'
const count = ref(0)
// flush: 'sync',同步执行
watchEffect(
() => {
console.log(`sync - count的值: ${count.value}`)
// 立即执行,不等待微任务队列
},
{ flush: 'sync' }
)
count.value++
console.log('同步执行完成')
// 输出顺序: sync - count的值: 1 -> 同步执行完成

⚠️ 注意:sync模式可能会导致性能问题和数据不一致,应当谨慎使用【turn0search7】。

3.2 调试选项:onTrack和onTrigger

watchEffect提供了两个调试选项onTrack和onTrigger,仅在开发模式下工作,用于调试侦听器的行为【turn0search5】。

3.2.1 onTrack

onTrack会在响应式property或ref作为依赖项被追踪时被调用【turn0search13】。

import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
watchEffect(
() => {
console.log(count.value + message.value)
},
{
onTrack(e) {
// 当依赖被追踪时调用
console.log('正在追踪依赖:', e)
// e包含target(目标对象)、type(追踪类型)和key(属性名)等信息
debugger // 可以在这里设置断点调试
}
}
)
3.2.2 onTrigger

onTrigger会在依赖项变更导致副作用被触发时被调用【turn0search13】。

import { ref, watchEffect } from 'vue'
const count = ref(0)
watchEffect(
() => {
console.log(count.value)
},
{
onTrigger(e) {
// 当依赖变更触发副作用时调用
console.log('依赖变更触发副作用:', e)
// e包含target、type、key、oldValue和newValue等信息
debugger // 可以在这里设置断点调试
}
}
)
count.value++ // 会触发onTrigger

四、watchEffect的高级用法

4.1 副作用清理:onInvalidate

watchEffect的副作用函数可以接收一个onInvalidate函数作为参数,用于注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求【turn0search3】。

import { ref, watchEffect } from 'vue'
const userId = ref(1)
watchEffect((onInvalidate) => {
// 模拟异步请求
const timer = setTimeout(() => {
console.log(`获取用户${userId.value}的数据`)
}, 1000)
// 注册清理函数
onInvalidate(() => {
// 在副作用重新执行前调用
clearTimeout(timer) // 清除上一次的定时器
console.log(`清除用户${userId.value}的请求`)
})
})
// 2秒后改变userId值
setTimeout(() => {
userId.value = 2
}, 2000)

4.2 停止侦听

watchEffect返回一个用于停止该副作用的函数,调用这个函数可以停止侦听【turn0search3】。

import { ref, watchEffect } from 'vue'
const count = ref(0)
// 启动侦听并获取停止函数
const stop = watchEffect(() => {
console.log(`count的值: ${count.value}`)
})
// 改变count值,会触发watchEffect
count.value++ // 输出: count的值: 1
// 停止侦听
stop()
// 再次改变count值,不会触发watchEffect
count.value++ // 无输出

4.3 watchPostEffect和watchSyncEffect

Vue3还提供了两个带预设flush选项的便捷方法:watchPostEffect(flush: ‘post’)和watchSyncEffect(flush: ‘sync’)【turn0search9】。

import { ref, watchPostEffect, watchSyncEffect } from 'vue'
const count = ref(0)
// 等同于watchEffect(..., { flush: 'post' })
watchPostEffect(() => {
console.log(`post effect: ${count.value}`)
})
// 等同于watchEffect(..., { flush: 'sync' })
watchSyncEffect(() => {
console.log(`sync effect: ${count.value}`)
})

五、watchEffect的实际应用场景

5.1 自动保存用户输入

在表单应用中,可以使用watchEffect自动保存用户输入到本地存储【turn0search2】。


<script setup>
import { ref, watchEffect } from 'vue'
const userInput = ref('')
// 自动保存到本地存储
watchEffect(() => {
  if (userInput.value.trim()) {
    localStorage.setItem('userInput', userInput.value)
    console.log('已保存到本地存储:', userInput.value)
  }
})
// 页面加载时从本地存储恢复
const savedInput = localStorage.getItem('userInput')
if (savedInput) {
  userInput.value = savedInput
}
</script>

5.2 响应式DOM操作

当需要根据响应式数据变化来操作DOM时,watchEffect非常方便【turn0search2】。


<script setup>
import { ref, onMounted, watchEffect } from 'vue'
const windowWidth = ref(window.innerWidth)
const resizeDiv = ref(null)
// 监听窗口大小变化
onMounted(() => {
  window.addEventListener('resize', () => {
    windowWidth.value = window.innerWidth
  })
})
// 根据窗口宽度调整DOM元素
watchEffect(() => {
  if (resizeDiv.value) {
    if (windowWidth.value < 768) {
      resizeDiv.value.style.backgroundColor = 'lightcoral'
      resizeDiv.value.style.height = '50px'
    } else {
      resizeDiv.value.style.backgroundColor = 'lightblue'
      resizeDiv.value.style.height = '100px'
    }
  }
})
</script>

5.3 路由参数监听

在单页应用中,可以使用watchEffect监听路由参数变化并重新获取数据【turn0search2】。


<script setup>
import { ref, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const userId = ref(route.params.id)
const userInfo = ref({ name: '', email: '' })
// 模拟获取用户数据的函数
const fetchUserData = (id) => {
  console.log(`获取用户${id}的数据`)
  // 这里应该是实际的API调用
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: `用户${id}`,
        email: `user${id}@example.com`
      })
    }, 500)
  })
}
// 监听路由参数变化
watchEffect(async () => {
  const newUserId = route.params.id
  if (newUserId !== userId.value) {
    userId.value = newUserId
    userInfo.value = await fetchUserData(newUserId)
  }
})
</script>

5.4 复杂计算逻辑

当计算逻辑依赖于多个响应式数据,且不需要返回值时,watchEffect比computed更合适【turn0search4】。


<script setup>
import { reactive, watchEffect, ref } from 'vue'
const cartItems = reactive([
  { id: 1, name: '商品A', price: 100, quantity: 1 },
  { id: 2, name: '商品B', price: 200, quantity: 2 }
])
const totalPrice = ref(0)
const shipping = ref(0)
const finalTotal = ref(0)
// 计算总价和运费
watchEffect(() => {
  // 计算商品总价
  const subtotal = cartItems.reduce((total, item) => {
    return total + item.price * item.quantity
  }, 0)
  totalPrice.value = subtotal
  // 根据总价计算运费
  if (subtotal >= 500) {
    shipping.value = 0
  } else if (subtotal >= 200) {
    shipping.value = 10
  } else {
    shipping.value = 20
  }
  // 计算最终总额
  finalTotal.value = subtotal + shipping.value
  // 可以在这里执行其他副作用,如记录日志
  console.log(`购物车更新: 总价¥${totalPrice.value}, 运费¥${shipping.value}, 应付¥${finalTotal.value}`)
})
</script>

六、watchEffect与watch的深度对比

6.1 核心差异分析

虽然watchEffect和watch都是用于侦听响应式数据变化的API,但它们在设计理念和使用方式上有本质区别【turn0search5】。

侦听需求
需要精确控制依赖?
使用watch
需要立即执行?
使用watchEffect
使用watch
明确指定数据源
自动收集依赖
获取新旧值
仅获取当前值
惰性执行
立即执行

6.2 使用场景对比

场景推荐使用原因
需要获取新旧值watchwatch提供新旧值参数
依赖关系复杂watchEffect自动收集依赖,代码更简洁
需要惰性执行watchwatch默认懒执行
需要立即执行watchEffectwatchEffect立即执行一次
需要精确控制依赖watch手动指定依赖,更可控
调试依赖关系watchEffect提供onTrack和onTrigger钩子

6.3 性能考虑

在性能方面,watch和watchEffect各有优势:

  • watchEffect:由于自动收集依赖,可能会追踪不必要的响应式数据,导致过度执行【turn0search6】
  • watch:手动指定依赖,可以精确控制回调执行时机,性能更可控【turn0search11】
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// watchEffect会追踪所有使用的响应式数据
watchEffect(() => {
console.log(count.value) // 即使只关心count,message变化也会触发重新执行
})
// watch只追踪指定的数据源
watch(count, () => {
console.log(count.value) // 只有count变化才会触发
})

七、watchEffect的内部实现原理

7.1 响应式追踪机制

watchEffect的底层实现基于Vue3的响应式系统,核心是使用ReactiveEffect类来管理副作用函数和依赖关系【turn0search16】。

// 简化的watchEffect实现原理
function watchEffect(effect, options = {}) {
// 创建副作用函数
const runner = effect(effect, {
lazy: false, // 立即执行
scheduler: job => {
// 调度器,在依赖变化时调用
queueJob(job)
},
...options
})
// 立即执行一次,建立依赖关系
runner()
// 返回停止函数
return () => {
stop(runner)
}
}

7.2 依赖收集过程

当watchEffect执行时,会触发响应式数据的getter,此时会进行依赖收集【turn0search16】。

watchEffect Effect Reactive Data Dep 创建副作用 访问响应式数据 触发getter 收集依赖 建立联系 依赖收集完成 数据变化 通知更新 重新执行 watchEffect Effect Reactive Data Dep

7.3 清理机制

watchEffect的清理机制通过onInvalidate函数实现,确保在副作用重新执行前清理之前的副作用【turn0search17】。

// 简化的清理机制实现
function watchEffect(effect) {
let cleanup
const runner = effect(() => {
// 执行清理函数
if (cleanup) {
cleanup()
}
// 调用用户函数,并传入清理函数注册器
effect(onInvalidate => {
cleanup = onInvalidate
})
})
return () => {
// 停止侦听时也执行清理
if (cleanup) {
cleanup()
}
stop(runner)
}
}

八、最佳实践与常见陷阱

8.1 最佳实践

8.1.1 合理使用flush选项

根据实际需求选择合适的flush选项,避免不必要的性能开销【turn0search1】。

// 默认pre:适用于大多数场景
watchEffect(() => {
// 默认行为,组件更新前执行
})
// post:需要访问更新后的DOM
watchEffect(() => {
// 操作DOM
}, { flush: 'post' })
// sync:谨慎使用,仅在必要时
watchEffect(() => {
// 同步执行
}, { flush: 'sync' })
8.1.2 及时清理副作用

使用onInvalidate清理副作用,避免内存泄漏和无效操作【turn0search3】。

watchEffect((onInvalidate) => {
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => {
// 处理数据
})
// 注册清理函数
onInvalidate(() => {
controller.abort() // 取消请求
})
})
8.1.3 避免在副作用中修改依赖

在副作用中修改被侦听的响应式数据可能导致无限循环【turn0search4】。

const count = ref(0)
// 可能导致无限循环
watchEffect(() => {
if (count.value < 10) {
count.value++ // 修改了被侦听的数据
}
})
// 正确做法:使用watch或添加条件判断
watch(count, (newValue) => {
if (newValue < 10) {
count.value++
}
})

8.2 常见陷阱

8.2.1 异步操作的依赖追踪

watchEffect仅在其同步执行期间才追踪依赖,使用异步回调时,只有在第一个await之前访问到的依赖才会被追踪【turn0search5】。

const count = ref(0)
const message = ref('Hello')
// 错误:message不会被追踪
watchEffect(async () => {
await new Promise(resolve => setTimeout(resolve, 100))
console.log(message.value) // 这个依赖不会被追踪
})
// 正确:在await前访问
watchEffect(async () => {
console.log(message.value) // 会被追踪
await new Promise(resolve => setTimeout(resolve, 100))
console.log(count.value) // 不会被追踪
})
8.2.2 深度监听的性能问题

对于大型对象,watchEffect的深度监听可能导致性能问题【turn0search11】。

const largeData = reactive({
// 大量嵌套数据
})
// 可能导致性能问题
watchEffect(() => {
// 访问大型对象会触发深度监听
console.log(largeData)
})
// 优化:只监听需要的属性
watchEffect(() => {
console.log(largeData.importantProperty)
})
8.2.3 停止侦听的时机

忘记在组件卸载时停止侦听可能导致内存泄漏【turn0search3】。

<script setup>
import { ref, watchEffect, onUnmounted } from 'vue'
const data = ref(0)
const stop = watchEffect(() => {
  console.log(data.value)
})
// 组件卸载时停止侦听
onUnmounted(() => {
  stop()
})
</script>

watchEffect作为Vue3 Composition API中的重要组成部分,为我们提供了一种简洁而强大的响应式编程方式。掌握它的特性和最佳实践,将有助于我们构建更加高效、可维护的Vue3应用。