Vue3侦听器实战:组件与Pinia状态监听如何高效应用?

69 阅读4分钟

Vue3 侦听器实战案例——在组件与Pinia中的应用

一、组件内的侦听器基础

1.1 基本概念与使用场景

侦听器是Vue3中用于响应数据变化的核心工具,当你需要在数据变化时执行异步或复杂的操作时,侦听器就派上用场了。比如:

  • 数据变化时发送API请求
  • 表单验证
  • 复杂的状态联动
Options API 写法
export default {
  data() {
    return {
      question: '',
      answer: 'Questions usually contain a question mark. ;-)',
      loading: false
    }
  },
  watch: {
    // 当question变化时触发
    question(newQuestion, oldQuestion) {
      if (newQuestion.includes('?')) {
        this.getAnswer()
      }
    }
  },
  methods: {
    async getAnswer() {
      this.loading = true
      this.answer = 'Thinking...'
      try {
        const res = await fetch('https://yesno.wtf/api')
        this.answer = (await res.json()).answer
      } catch (error) {
        this.answer = 'Error! Could not reach the API. ' + error
      } finally {
        this.loading = false
      }
    }
  }
}
Composition API 写法
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// 直接监听ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>

1.2 深度侦听器

默认情况下,侦听器是浅层的,只会监听引用类型的引用变化,不会监听内部属性变化。如果需要监听嵌套对象的变化,需要使用深度侦听器。

import { reactive, watch } from 'vue'

const user = reactive({
  name: 'John',
  address: {
    city: 'New York'
  }
})

// 深度侦听整个对象
watch(user, (newUser, oldUser) => {
  console.log('User changed:', newUser)
}, { deep: true })

// 或者只侦听嵌套属性
watch(() => user.address.city, (newCity) => {
  console.log('City changed to:', newCity)
})

1.3 立即执行侦听器

默认情况下,侦听器是懒加载的,只有当数据变化时才会触发。如果需要在组件创建时立即执行一次,可以使用immediate: true选项。

watch(question, (newQuestion) => {
  // 这个回调会在组件创建时立即执行一次
}, { immediate: true })

1.4 watchEffect 的使用

watchEffect是Vue3提供的另一种侦听器,它会自动追踪回调函数中的响应式依赖,当依赖变化时自动触发。

import { ref, watchEffect } from 'vue'

const todoId = ref(1)
const data = ref(null)

// 自动追踪todoId的变化
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

watch vs watchEffect 对比流程图:

┌─────────────────┐     ┌─────────────────┐
│   watch         │     │   watchEffect   │
├─────────────────┤     ├─────────────────┤
│ 显式指定依赖    │     │ 自动追踪依赖    │
│ 懒加载(默认)  │     │ 立即执行        │
│ 可获取新旧值    │     │ 无法获取旧值    │
│ 更精确控制      │     │ 更简洁语法      │
└─────────────────┘     └─────────────────┘

二、Pinia中的侦听器应用

2.1 监听Pinia Store中的状态

在Pinia中,

往期文章归档
免费好用的热门在线工具
我们可以使用Vue的`watch`函数来监听store中的状态变化。

首先创建一个Pinia store:

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    message: 'Hello Pinia'
  }),
  actions: {
    increment() {
      this.count++
    }
  }
})

然后在组件中监听store状态:

<script setup>
import { watch } from 'vue'
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

// 监听单个状态
watch(() => counterStore.count, (newCount, oldCount) => {
  console.log(`Count changed from ${oldCount} to ${newCount}`)
})

// 监听多个状态
watch([() => counterStore.count, () => counterStore.message], 
  ([newCount, newMessage], [oldCount, oldMessage]) => {
    console.log(`Count: ${oldCount} -> ${newCount}`)
    console.log(`Message: ${oldMessage} -> ${newMessage}`)
  }
)

// 深度监听整个store
watch(counterStore, (newStore, oldStore) => {
  console.log('Store changed:', newStore)
}, { deep: true })
</script>

2.2 实战案例:表单状态同步

假设我们有一个表单组件,需要将表单数据同步到Pinia store中,同时当store中的数据变化时,表单也需要更新。

<script setup>
import { ref, watch } from 'vue'
import { useFormStore } from '@/stores/form'

const formStore = useFormStore()

// 本地表单数据
const localForm = ref({
  name: '',
  email: ''
})

// 组件创建时同步store数据到本地
localForm.value = { ...formStore.formData }

// 监听本地表单变化,同步到store
watch(localForm, (newForm) => {
  formStore.updateFormData(newForm)
}, { deep: true })

// 监听store数据变化,同步到本地
watch(() => formStore.formData, (newForm) => {
  localForm.value = { ...newForm }
}, { deep: true })
</script>

<template>
  <form>
    <input v-model="localForm.name" placeholder="Name" />
    <input v-model="localForm.email" placeholder="Email" type="email" />
  </form>
</template>

Pinia store代码:

// stores/form.js
import { defineStore } from 'pinia'

export const useFormStore = defineStore('form', {
  state: () => ({
    formData: {
      name: '',
      email: ''
    }
  }),
  actions: {
    updateFormData(newData) {
      this.formData = { ...newData }
    }
  }
})

三、课后Quiz

问题1:watch和watchEffect的主要区别是什么?

答案解析:

  1. 依赖追踪方式:watch需要显式指定依赖,而watchEffect会自动追踪回调中的响应式依赖。
  2. 执行时机:watch默认是懒加载的,只有当依赖变化时才会触发;watchEffect会在组件创建时立即执行一次。
  3. 参数获取:watch可以获取新旧值,而watchEffect无法获取旧值。
  4. 使用场景:watch适合需要精确控制依赖和获取新旧值的场景;watchEffect适合简单的副作用处理,语法更简洁。

问题2:如何在Pinia中监听整个store的变化?

答案解析: 可以直接将store实例作为watch的第一个参数,并设置deep: true选项:

watch(counterStore, (newStore, oldStore) => {
  console.log('Store changed:', newStore)
}, { deep: true })

或者使用getter函数:

watch(() => ({ ...counterStore }), (newStore) => {
  console.log('Store changed:', newStore)
})

四、常见报错解决方案

1. 报错:"watch() expects a source value as the first argument"

原因:传递给watch的第一个参数不是有效的响应式源。比如直接传递了一个非响应式的对象或值。 解决方法

  • 确保监听的是ref、reactive对象,或者返回响应式值的getter函数。
  • 如果要监听对象的某个属性,使用getter函数:watch(() => obj.property, callback)

2. 报错:"deep option is ignored when watching a getter"

原因:当使用getter函数作为watch源时,deep选项会被忽略,因为getter函数返回的是一个值,而不是引用类型。 解决方法

  • 如果需要深度监听,直接监听整个reactive对象,或者在getter函数中返回对象的副本:watch(() => ({ ...obj }), callback, { deep: true })

3. 报错:"Maximum call stack size exceeded"

原因:在watch回调中修改了被监听的数据,导致无限循环。 解决方法

  • 确保在watch回调中不会修改被监听的同一个数据。
  • 如果需要修改,可以使用条件判断或者使用watchEffect代替。

4. Pinia中watch不触发

原因:可能是因为没有正确使用getter函数来监听store中的状态。 解决方法

  • 使用getter函数来监听store中的状态:watch(() => store.count, callback)
  • 确保store中的状态是响应式的,使用Pinia的state定义方式。

五、参考链接