Vue 3 的watchEffect函数:介绍watchEffect的基本用法和特点

🎪 前端摸鱼匠:个人主页

🎒 个人专栏:《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应用。

相关推荐
拉不动的猪2 小时前
基本数据类型Symbol的基本应用场景
前端·javascript·面试
天庭鸡腿哥2 小时前
谷歌出品,堪称手机版PS!
javascript·智能手机·eclipse·maven
_小九2 小时前
【开源】耗时数月、我开发了一款功能全面【30W行代码】的AI图床
前端·后端·开源
just小千2 小时前
HTML进阶——常用标签及其属性
前端·html
惜.己2 小时前
html笔记(一)
前端·笔记·html
Lsx-codeShare2 小时前
一文读懂 Uniapp 小程序登录流程
前端·javascript·小程序·uni-app
曹牧2 小时前
HTML实体名称
前端·html
小胖霞2 小时前
Node+Express+MySQL 后端生产环境部署,实现注册功能(三)
前端·后端