
🎪 前端摸鱼匠:个人主页
🎒 个人专栏:《vue3入门到精通》
🥇 没有好的理念,只有脚踏实地!
文章目录
-
- 一、初识watchEffect:响应式编程的利器
-
- [1.1 什么是watchEffect](#1.1 什么是watchEffect)
- [1.2 watchEffect的核心特点](#1.2 watchEffect的核心特点)
- [1.3 与watch的初步对比](#1.3 与watch的初步对比)
- 二、watchEffect的基本用法
-
- [2.1 基础语法结构](#2.1 基础语法结构)
- [2.2 监听ref类型数据](#2.2 监听ref类型数据)
- [2.3 监听reactive类型数据](#2.3 监听reactive类型数据)
- [2.4 监听多个数据源](#2.4 监听多个数据源)
- 三、watchEffect的配置选项
-
- [3.1 flush选项:控制执行时机](#3.1 flush选项:控制执行时机)
-
- [3.1.1 flush: 'pre'(默认值)](#3.1.1 flush: 'pre'(默认值))
- [3.1.2 flush: 'post'](#3.1.2 flush: 'post')
- [3.1.3 flush: 'sync'](#3.1.3 flush: 'sync')
- [3.2 调试选项:onTrack和onTrigger](#3.2 调试选项:onTrack和onTrigger)
-
- [3.2.1 onTrack](#3.2.1 onTrack)
- [3.2.2 onTrigger](#3.2.2 onTrigger)
- 四、watchEffect的高级用法
-
- [4.1 副作用清理:onInvalidate](#4.1 副作用清理:onInvalidate)
- [4.2 停止侦听](#4.2 停止侦听)
- [4.3 watchPostEffect和watchSyncEffect](#4.3 watchPostEffect和watchSyncEffect)
- 五、watchEffect的实际应用场景
-
- [5.1 自动保存用户输入](#5.1 自动保存用户输入)
- [5.2 响应式DOM操作](#5.2 响应式DOM操作)
- [5.3 路由参数监听](#5.3 路由参数监听)
- [5.4 复杂计算逻辑](#5.4 复杂计算逻辑)
- 六、watchEffect与watch的深度对比
-
- [6.1 核心差异分析](#6.1 核心差异分析)
- [6.2 使用场景对比](#6.2 使用场景对比)
- [6.3 性能考虑](#6.3 性能考虑)
- 七、watchEffect的内部实现原理
-
- [7.1 响应式追踪机制](#7.1 响应式追踪机制)
- [7.2 依赖收集过程](#7.2 依赖收集过程)
- [7.3 清理机制](#7.3 清理机制)
- 八、最佳实践与常见陷阱
-
- [8.1 最佳实践](#8.1 最佳实践)
-
- [8.1.1 合理使用flush选项](#8.1.1 合理使用flush选项)
- [8.1.2 及时清理副作用](#8.1.2 及时清理副作用)
- [8.1.3 避免在副作用中修改依赖](#8.1.3 避免在副作用中修改依赖)
- [8.2 常见陷阱](#8.2 常见陷阱)
-
- [8.2.1 异步操作的依赖追踪](#8.2.1 异步操作的依赖追踪)
- [8.2.2 深度监听的性能问题](#8.2.2 深度监听的性能问题)
- [8.2.3 停止侦听的时机](#8.2.3 停止侦听的时机)
一、初识watchEffect:响应式编程的利器
1.1 什么是watchEffect
在Vue3的Composition API中,watchEffect是一个极其强大的响应式API,它为我们提供了一种自动追踪依赖并执行副作用的方式。根据官方文档的定义,watchEffect会立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行【turn0search1】。这意味着我们不需要显式地指定要监听的数据源,watchEffect会自动检测函数内部使用的响应式数据,并在这些数据变化时重新运行函数。
javascript
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都能监听数据变化,但它们在设计理念上有明显区别:
| 特性 | watchEffect | watch |
|---|---|---|
| 依赖追踪 | 自动收集 | 手动指定 |
| 懒执行 | 否(立即执行) | 是(默认) |
| 获取新旧值 | 否 | 是 |
| 使用场景 | 依赖关系复杂 | 需要精确控制 |
二、watchEffect的基本用法
2.1 基础语法结构
watchEffect的基本语法非常简洁,接受两个参数:一个副作用函数和一个可选的配置对象【turn0search3】。
javascript
// 基本语法
watchEffect(
() => {
// 副作用函数内容
},
{
// 可选配置项
flush: 'pre', // 'pre' | 'post' | 'sync'
onTrack: (e) => {}, // 调试钩子
onTrigger: (e) => {} // 调试钩子
}
)
2.2 监听ref类型数据
当监听ref定义的基本类型数据时,watchEffect会自动追踪其value属性的变化【turn0search1】。
vue
<template>
<div>
<p>计数器: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<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】。
vue
<template>
<div>
<p>姓名: {{ user.name }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="updateUser">更新用户信息</button>
</div>
</template>
<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】。
javascript
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】。
javascript
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】。
javascript
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】。这意味着副作用会立即在响应式数据变化时执行。
javascript
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】。
javascript
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】。
javascript
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】。
javascript
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】。
javascript
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】。
javascript
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】。
vue
<template>
<div>
<input v-model="userInput" placeholder="输入内容..." />
<p>输入内容: {{ userInput }}</p>
</div>
</template>
<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】。
vue
<template>
<div>
<p>窗口宽度: {{ windowWidth }}px</p>
<div ref="resizeDiv" class="resize-div"></div>
</div>
</template>
<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>
<style>
.resize-div {
width: 100%;
transition: all 0.3s ease;
}
</style>
5.3 路由参数监听
在单页应用中,可以使用watchEffect监听路由参数变化并重新获取数据【turn0search2】。
vue
<template>
<div>
<h1>用户详情</h1>
<p>用户ID: {{ userId }}</p>
<p>用户名: {{ userInfo.name }}</p>
<p>邮箱: {{ userInfo.email }}</p>
</div>
</template>
<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】。
vue
<template>
<div>
<h2>购物车</h2>
<div v-for="item in cartItems" :key="item.id">
<span>{{ item.name }}</span>
<span>单价: ¥{{ item.price }}</span>
<input v-model.number="item.quantity" type="number" min="1" />
<span>小计: ¥{{ item.price * item.quantity }}</span>
</div>
<p>总计: ¥{{ totalPrice }}</p>
<p>运费: ¥{{ shipping }}</p>
<p>应付总额: ¥{{ finalTotal }}</p>
</div>
</template>
<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 使用场景对比
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 需要获取新旧值 | watch | watch提供新旧值参数 |
| 依赖关系复杂 | watchEffect | 自动收集依赖,代码更简洁 |
| 需要惰性执行 | watch | watch默认懒执行 |
| 需要立即执行 | watchEffect | watchEffect立即执行一次 |
| 需要精确控制依赖 | watch | 手动指定依赖,更可控 |
| 调试依赖关系 | watchEffect | 提供onTrack和onTrigger钩子 |
6.3 性能考虑
在性能方面,watch和watchEffect各有优势:
- watchEffect:由于自动收集依赖,可能会追踪不必要的响应式数据,导致过度执行【turn0search6】
- watch:手动指定依赖,可以精确控制回调执行时机,性能更可控【turn0search11】
javascript
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】。
javascript
// 简化的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】。
javascript
// 简化的清理机制实现
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】。
javascript
// 默认pre:适用于大多数场景
watchEffect(() => {
// 默认行为,组件更新前执行
})
// post:需要访问更新后的DOM
watchEffect(() => {
// 操作DOM
}, { flush: 'post' })
// sync:谨慎使用,仅在必要时
watchEffect(() => {
// 同步执行
}, { flush: 'sync' })
8.1.2 及时清理副作用
使用onInvalidate清理副作用,避免内存泄漏和无效操作【turn0search3】。
javascript
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】。
javascript
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】。
javascript
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】。
javascript
const largeData = reactive({
// 大量嵌套数据
})
// 可能导致性能问题
watchEffect(() => {
// 访问大型对象会触发深度监听
console.log(largeData)
})
// 优化:只监听需要的属性
watchEffect(() => {
console.log(largeData.importantProperty)
})
8.2.3 停止侦听的时机
忘记在组件卸载时停止侦听可能导致内存泄漏【turn0search3】。
vue
<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应用。