Code前端首页关于Code前端联系我们

Vue3怎么监听Pinia/Vuex store里的状态变化?新手必看避坑指南

terry 2小时前 阅读数 34 #Vue

Vue3为什么要监听store的状态变化?

你刚接触Vue3开发,可能会有个疑问:直接把store里的state用computed取出来渲染不行吗?为什么还要单独加watch?其实这个场景还挺常见的——比如状态改变后要触发额外的非UI逻辑,像是向后台埋个数据埋点、更新浏览器的localStorage缓存、播放一段提示音、或者调整路由参数这类“副作用”操作,computed是做不到的,它只能返回计算后的结果给UI,这时候就需要Vue3的watch或者watchEffect来配合store了。

Vue3监听Pinia store状态的两种主流方式

Pinia作为Vue3官方力推的“下一代Vuex”,API设计得更简洁,和Vue3的组合式API(Setup语法)适配得也更丝滑,监听它的状态变化,主要有两种思路:一种是直接监听store实例的某个具体属性,另一种是监听整个store或者通过$subscribe监听mutation/commit的触发(不过现在Pinia里已经叫action了,但$subscribe也保留了对state变化的跟踪)。

用Vue3原生的watch直接监听单个/多个state

这种是最常用、最直观的方法,适合只需要关注几个特定状态值变化的场景,你得先注意一点:Pinia的store实例里的state默认是响应式的,但如果你直接把整个store的state解构出来赋值给普通变量,那响应式就会丢失——所以要么直接用store.state.xxx,要么用storeToRefs把需要的state属性转成ref再解构。

举个Setup语法的例子:假设你有个userStore,里面存了userInfo(用户信息对象)和token(登录令牌字符串)两个state。

<script setup>
import { watch, watchEffect } from 'vue'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
// 初始化store
const userStore = useUserStore()
// 用storeToRefs转ref,保留响应式再解构
const { userInfo, token } = storeToRefs(userStore)
// 1. 监听单个原始值类型的state(比如字符串token)
// 第三个参数是可选的配置项,比如immediate: true可以让watch初始化时先执行一次
watch(token, (newVal, oldVal) => {
  console.log('token变啦!旧值:', oldVal, '新值:', newVal)
  // 这里加副作用:比如token变了就更新localStorage
  if (newVal) {
    localStorage.setItem('token', newVal)
  } else {
    localStorage.removeItem('token')
  }
}, { immediate: true })
// 2. 监听单个引用值类型的state(比如对象userInfo)
// 注意:默认情况下watch只监听引用值的“引用地址”变化,比如整个userInfo被重新赋值才会触发
// 如果要监听对象内部某个属性甚至所有属性的变化,得加deep: true配置项
watch(
  () => userInfo.value.nickname, // 也可以只监听对象的某个具体属性,用函数返回
  (newNickname, oldNickname) => {
    console.log('用户昵称改了!新昵称:', newNickname)
    // 埋个用户修改昵称的埋点
    // trackEvent('user_nickname_update', { old: oldNickname, new: newNickname })
  }
)
// 如果监听整个userInfo内部所有属性
watch(
  userInfo,
  (newInfo, oldInfo) => {
    console.log('用户信息整体或内部属性变了')
  },
  { deep: true }
)
// 3. 同时监听多个state
// 可以把多个ref或者函数返回的属性放在一个数组里
watch(
  [token, () => userInfo.value.level],
  ([newToken, newLevel], [oldToken, oldLevel]) => {
    console.log('token或用户等级变了')
  }
)
</script>

这里补充个新手容易踩的坑:直接解构storeToRefs后的对象没问题,但如果你只解构useUserStore()的返回值,也就是const { userInfo, token } = useUserStore(),那userInfo和token都是普通对象/字符串,失去了响应式,watch根本不会触发,这点一定要记住。

用Pinia自带的$subscribe API

这种方法更适合监听整个store的所有state变化,或者需要知道具体是哪个state的哪个属性被修改了的场景——比如你要把整个store的state实时同步到localStorage、sessionStorage甚至IndexedDB里,用$subscribe就比写一堆watch要方便得多。

$subscribe的回调函数有两个参数:第一个是mutation对象(虽然Pinia里没有显式的mutation,但底层还是用的类似Vuex mutation的机制更新state,这个对象会告诉我们修改的路径、类型等等),第二个是修改后的整个store的state。

继续用刚才的userStore举例子:

<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
// 实时同步整个userStore的state到localStorage
userStore.$subscribe((mutation, state) => {
  console.log('Pinia触发了state更新,路径:', mutation.payload.path)
  // 把整个state转成JSON字符串存进去
  localStorage.setItem('userStore', JSON.stringify(state))
}, {
  // 配置项:detached: true,默认是false,false的话当组件卸载时这个订阅会自动取消
  // true的话即使组件卸载,订阅依然存在,适合全局的缓存同步
  detached: true
})
</script>

这里的detached配置项也很重要:如果不加,默认是绑定到当前组件的生命周期的,组件销毁后监听就没了;加了的话就变成全局监听了,不管在哪个组件触发state变化,这个回调都会执行,那什么时候用默认什么时候用detached呢?比如只在某个详情页监听userInfo里的某个字段是否被修改,提示用户是否保存,这时候用默认的就行,页面关了不需要再提示;但像刚才的全局缓存同步,就得加detached。

万一还在用旧项目的Vuex呢?Vue3怎么监听Vuex4的状态变化?

虽然Pinia是推荐,但很多公司的老项目可能还是用的Vuex4(适配Vue3的Vuex版本),所以我们也得提一下Vuex4的监听方法,其实和Pinia类似,但有一点小区别。

用Vue3原生watch配合Vuex的state

同样,直接从useStore()里解构state也会失去响应式吗?其实Vuex4的useStore()返回的store实例里的state是一个reactive对象,所以你可以直接用() => store.state.user.token这种函数返回值的方式监听,或者用toRefs把store.state转成ref再解构,这和Pinia的storeToRefs有点像,但用途不同——toRefs是转任意reactive对象,storeToRefs是专门转Pinia store实例,它会把state、getters都转成ref,action还是保留原来的函数形式,更方便。

举个Vuex4的例子:

<script setup>
import { watch, toRefs } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// 转整个state成ref,也可以只转需要的
const { user } = toRefs(store.state)
// 监听单个原始值
watch(
  () => store.state.user.token,
  (newVal, oldVal) => {
    console.log('Vuex4 token变了')
    localStorage.setItem('token', newVal)
  },
  { immediate: true }
)
// 监听引用值内部属性
watch(
  () => user.value.nickname,
  (newNick) => {
    console.log('Vuex4 用户昵称变了')
  }
)
</script>

用Vuex4自带的subscribe/subscribeAction API

Vuex4保留了Vuex3的两个订阅API:subscribe和subscribeAction,subscribe和Pinia的$subscribe很像,是监听mutation的提交,回调参数也是mutation对象和修改后的state;subscribeAction是监听action的触发,回调参数有两个,一个是action对象(包含type和payload),另一个是store实例,它还可以配置before和after来分别在action执行前和执行后触发。

全局缓存同步用subscribe就行:

<script setup>
import { useStore } from 'vuex'
const store = useStore()
const unsubscribe = store.subscribe((mutation, state) => {
  console.log('Vuex4 mutation提交了', mutation.type)
  localStorage.setItem('vuexStore', JSON.stringify(state))
})
// 注意:Vuex4的subscribe没有Pinia的detached配置项!
// 如果要取消订阅,得手动调用返回的unsubscribe函数,比如在组件的onUnmounted里
// import { onUnmounted } from 'vue'
// onUnmounted(() => {
//   unsubscribe()
// })
</script>

subscribeAction的before/after配置举个简单的例子:比如在提交登录请求的action执行前显示加载动画,执行后隐藏:

<script setup>
import { ref, onUnmounted } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const loading = ref(false)
const unsubscribeAction = store.subscribeAction({
  before: (action) => {
    if (action.type === 'user/login') {
      loading.value = true
    }
  },
  after: (action) => {
    if (action.type === 'user/login') {
      loading.value = false
    }
  }
})
onUnmounted(() => {
  unsubscribeAction()
})
</script>

再补充几个通用的避坑点,不管是Pinia还是Vuex都适用

  1. 不要滥用deep: true:deep配置虽然能监听引用值内部的所有变化,但性能开销很大,尤其是当你监听的是一个非常大的数组或对象时,每次修改内部任何一个小属性都会触发整个对象的深度遍历,影响页面的流畅度,所以最好的做法是:能监听单个属性就监听单个属性,实在不行再监听整个对象并加deep,或者用更细粒度的state拆分。
  2. immediate配置要谨慎用:immediate: true会让watch在组件初始化时就执行一次回调,这时候newVal有值,但oldVal是undefined,如果你在回调里用到了oldVal做判断,记得加个oldVal !== undefined的条件,不然可能会出现bug。
  3. 避免在watch回调里直接修改监听的state:这会形成无限循环!比如你监听token,在回调里又去改token,改完token又触发watch,watch又改token,无限下去,页面直接卡死。
  4. 组件卸载时记得取消不必要的全局订阅:虽然Pinia的$subscribe默认是绑定到组件的,但加了detached就不会了;Vuex4的subscribe和subscribeAction完全需要手动取消,如果不取消,这些订阅会一直存在,占用内存,甚至可能触发不需要的副作用。
  5. watchEffect和watch的区别要搞清楚:watchEffect是自动收集依赖的,只要回调里用到的响应式数据变了,它就会触发,不需要手动指定监听的对象;而watch是手动指定监听对象的,只有指定的对象变了才会触发,如果你只需要在特定数据变化时执行副作用,用watch更可控;如果你需要自动收集一堆相关数据的变化,用watchEffect更方便,但要注意不要引入不必要的依赖。

该选哪种方式监听store的状态变化?

  • 只关注单个/几个特定state的变化,且需要知道旧值和新值:选Vue3原生的watch,配合toRefs(Vuex)或storeToRefs(Pinia)保留响应式。
  • 需要实时同步整个store的state到本地存储,或者需要知道具体的修改路径:选Pinia的$subscribe(加detached做全局同步)或Vuex4的subscribe(记得手动取消)。
  • 需要在action执行前后触发副作用(比如显示/隐藏加载动画):选Vuex4的subscribeAction,Pinia暂时没有官方的before/after subscribeAction,但可以在每个action里手动加,或者用Pinia的插件来实现类似的功能(这个是进阶内容,新手暂时不用掌握)。
  • 不需要知道旧值,且需要自动收集依赖:选watchEffect。

其实不管用哪种方式,核心都是要结合你的具体需求来选,不要盲目用deep或者全局订阅,尽量让代码更简洁、更可控、性能更好。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门