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

Vue3 怎么有效watch Pinia store?要避坑指南加3种常用场景全解析

terry 7小时前 阅读数 90 #Vue

今天刚好跟做Vue3+Pinia后台项目的学弟学妹聊到状态监听,发现很多人要么套了旧的watch写法,要么监听不了深层状态,要么性能卡得不行,问题出在哪?又该怎么把watch才算“丝滑又精准地实现需求?别急,咱们一步一步掰扯清楚。

为什么直接watch Pinia不能随便写旧方法会出问题?

很多刚转Pinia的朋友第一反应就是把store里的state直接拿出来当普通ref传进watch,比如直接写watch(() => userStore, (newVal) => console.log(newVal), 控制台要么没反应要么乱触发,踩坑踩得一脸懵。 这里的原因主要有两个: 第一个是Pinia的store默认是响应式代理对象的引用,但不是普通的ref,它的state本身不是Vue3里的基本响应式引用类型,直接传整个store的话,Vue的watch默认只监听引用变化,除非整个store对象本身被替换才会触发,但Pinia里都是修改都是state字段,一般不会有人没事替换整个store对吧?这就导致第一种“完全没反应”的情况; 第二个是很多朋友知道不能传整个store的话,就改成传userStore.state,但是又分了state的引用,这个时候又只监听的state的字段都是响应式了,但还有深层嵌套的对象还是需要加deep: true?不对,等下,不是所有情况都要加deep,加deep有什么坏处?后面避坑的时候讲,先把这个原因说完,哦对了还有,userStore是响应式代理,但是直接解构出来的变量,比如const { name, age } = userStore,那解构出来的name和age就不是响应式的了!除非用toRefs或者toRef把它们包起来,不然直接watch name或者watch age,也是白搭,很多新手就踩过这个坑。

为什么直接watch Pinia store的常规写法容易踩坑?

刚转Pinia+Vue3组合式API的朋友,大概率会踩第一个最经典的雷:直接把userStore这种store实例扔进watch里,比如写watch(userStore, (newVal) => console.log(newVal)),结果要么完全没触发,要么触发次数不对劲。 原因其实藏在Pinia和Vue3响应式的底层区别上:Pinia的store默认是用Proxy包装的“响应式代理对象”,但它是整个store的引用是稳定的,除非你手动用$reset之外的特殊替换操作硬把整个state覆盖掉?一般项目里都不会干这种事,那直接传整个store的watch,默认只监听引用地址的变化,当然不会因为state字段变了半天没反应。 第二个雷更隐蔽:很多人知道不能传整个store,就改成传userStore.state?不对,等下组合式API里直接用Pinia store解构出来的?哦不对哦不对,等下Pinia setup store(现在主流写setup store对吧)是直接把函数返回的是一个普通对象吗?不,不是,setup store返回的对象被Pinia内部用reactive包裹了整个对象包装成了带Pinia专属的响应式实例,不管是setup还是option setup还是option store,你直接用const { userName, userInfo } = userStore这种普通解构的话,userName如果是基本数据类型,就直接失去响应式了!除非用toRefs(userStore)或者单独toRef基本类型或者option store里的$state也不行setup store里没$state默认没有Pinia官方也给你准备好了,但不管哪种,直接普通解构是最大的坑最多人踩。

为什么直接watch Pinia store的常规写法容易踩坑?

刚转Pinia+Vue3组合式API的朋友,大概率会踩第一个最经典的雷:直接把userStore这种store实例扔进watch里,比如写watch(userStore, (newVal) => console.log(newVal)),结果要么完全没触发,要么触发次数不对劲。 原因其实藏在Pinia和Vue3响应式的底层和setup和option store的区别里?不,现在主流setup store更普遍,不管哪种都一样:Pinia的store默认是用Proxy包装的“稳定的响应式代理,除非你手动用$reset之外的极端操作硬把整个内部state对象覆盖掉?一般实际项目里都不会干这种事,默认的watch只监听引用地址的变化,当然不会因为state字段变了半天没反应。 第二个雷更隐蔽:很多人知道不能传整个store,就改成传userStore.state?不对,等下setup store是写在函数返回的对象被Pinia内部用reactive包装整个对象吗?不管是setup还是option store,你直接用const { userName, userInfo } = userStore这种普通解构的话,userName如果是string、number这种基本数据类型,就直接**直接失去响应式了!除非用toRefs(userStore)或者toRef(userStore, 'userName')这种Vue3官方提供的API把它们再“绑”回store上,不然直接watch普通解构出来的userName,也是白搭。

为什么直接watch Pinia store的常规写法容易踩坑?

刚转Pinia+Vue3组合式API的朋友,大概率会踩第一个最经典的雷:直接把userStore这种store实例扔进watch里,比如写watch(userStore, (newVal) => console.log(newVal)),结果要么完全没触发,要么触发次数很离谱。 原因藏在Pinia和Vue3响应式的底层特性上:不管是现在主流的setup store还是传统的option store,Pinia返回的都是一个用Proxy包装的稳定响应式代理实例——除非你手动用类似$reset之外的极端操作硬把整个内部state覆盖掉,一般实际项目里没人会干这种事,默认的watch只监听「引用地址的变化」,当然不会因为改了个userName、点了个收藏按钮这种常规state字段变更而触发。 第二个雷更隐蔽:很多人知道不能传整个store,就改成传userStore.state?不对,setup store根本没这个属性;还有人知道普通解构store的话,const { userName, userInfo } = userStore里的userName(基本数据类型)会失活,就随便加个deep:true监听整个userStore?加deep:true也不对,要么无效,要么性能炸锅——这些都是典型的新手误区。

最推荐的3种Pinia store精准watch方法,覆盖99%项目场景

废话不多说,直接上干货,每种方法配个场景说明什么时候用,更直观。

用getter或者箭头函数包裹需要监听的单个/多个字段

这是最常用、性能最优的写法,单个字段的话直接用箭头函数返回userStore.userName,多个的话就放在数组里,箭头函数的写法能精确告诉Vue3我只关注这几个值的变化,不会因为其他不相关的字段动一下就触发回调,也不用加deep(除非包裹的那个字段本身是深层嵌套对象,这个后面再说)。 举个场景:后台管理系统里,切换用户身份(userRole)的时候,要重新请求侧边栏菜单数据,这个时候就监听userStore.userRole就行: 组合式API里引入watch对吧?setup store里的话,或者import defineStore、useUserStore、watch这些都得引,直接写就行。 伪代码就大概是这样的,注意哦,不管是setup还是option,这个方法通用! 还有,监听多个字段的话,比如切换身份加切换语言(userLang)的时候,都要调整顶部导航栏的样式或者请求不同的资源,就可以这样写。

### 最推荐的3种Pinia store精准watch方法,覆盖99%项目场景 废话不多说,直接上干货,每种方法配个具体的后台或前端项目常用场景,什么时候用更直观。 #### 方法一:用箭头函数/getter包裹单个/多个字段 这是**性能最优、覆盖场景最广的写法**,没有之一!不管是单个字段还是几个不相关的字段,都能用箭头函数精确告诉Vue3:“我只盯着这几个值的变化,别管其他的”,不需要加多余的配置(除非包裹的那个/那几个字段里有深层嵌套的对象,这个后面单独说deep的小技巧)。 举个后台管理系统的高频场景:当用户切换身份(userStore.userRole)的时候,要清空当前页面的临时表单数据、重新请求对应身份的侧边栏目录,这个时候只监听userStore.userRole就行,完全不需要管store里的其他东西。 组合式API里的具体代码:先引入defineStore(已经定义好的useAppStore、useUserStore这些不算引入核心,核心是Vue3的watch和可能用到的toRefs/toRef,但这个方法暂时不需要),然后在setup或者script setup里写就行。 比如我之前写过的权限管理侧边栏代码片段(伪代码精简版,主要看watch的逻辑): ```javascript import { watch } from 'vue'; import { useUserStore } from '@/stores/user'; export default { setup() { const userStore = useUserStore(); // 单个字段监听 watch( () => userStore.userRole, (newRole, oldRole) => { console.log(`身份从${oldRole}切换到${newRole}啦!'); // 清空临时表单 const tempForm.value = {}; // 重新请求侧边栏 getSidebarMenu(newRole); }, // 可选配置 { immediate: true } // 首次加载组件时也会触发一次,比如用户刷新页面,刚拿到role就调接口拿侧边栏,很实用 ); return {}; } } // 如果是script setup更简单,不用return ``` 要是监听多个不相关的字段?比如切换身份加切换全局主题(appStore.themeColor)的时候,都要重新请求不同的全局样式组件?把它们放在数组里就行,回调的newVal和oldVal也会变成对应顺序的数组: ```javascript watch( [() => userStore.userRole, () => appStore.themeColor], ([newRole, newTheme], [oldRole, oldTheme] => { // 处理逻辑 } ); ``` 对了,还有更优雅点的话,也可以用getter!比如store里已经有getter叫curUserRole了,直接传curUserRole这个getter就行,不用再写箭头函数重复包裹,因为Pinia的getter本身就是computed属性,也是响应式的,这种写法更清晰,逻辑也集中在store里,符合Pinia的设计理念:“状态逻辑都在store,组件只负责用和渲染。 #### 方法二:用toRefs/toRef解构后直接传,适合频繁用,toRefs适合批量用 刚才说了普通解构会让基本数据类型的字段失活,那用toRefs或者toRef包一下呢?对,这个方法适合那种“既要频繁在组件里用这个字段,又要监听它变化”的场景,比如后台里的用户输入框绑定的是store里的userName,用户改名字的时候要触发实时校验,比如长度不能超过20位,有没有敏感词这种。 比如实时校验userName的敏感词: ```javascript import { watch, toRef, toRefs } from 'vue'; import { useUserStore } from '@/stores/user'; export default { setup() { const userStore = useUserStore(); // toRef单独用 const userName = toRef(userStore, 'userName'); // toRefs批量用,比如同时要用到userName、userEmail、userPhone这些多个字段,就可以const { userName, userEmail, userPhone } = toRefs(userStore); watch( userName, (newName) => { // 这里直接用newName就行,不用再写userStore.userName了 checkSensitiveWord(newName); }, { debounce: 300 } // 防抖,防止用户打字太快,频繁调接口,太实用了这个配置 ); return { userName // 直接绑定到input的v-model就行,双向绑定有效哦!这个是最大的优点,普通解构做不到的,toRef/toRefs可以 }; } } // script setup同理,return可以省略 ``` 这个方法的好处就是**既能直接v-model绑定,不用再写`v-model:userName`这种带冒号的写法,也能直接监听,一举两得,适合表单绑定store里的字段这种高频场景。 #### 方法三:监听整个$state,适合批量监听多个字段但又不想一个个写箭头函数的情况 刚才说了不能直接传整个userStore,那传`userStore.$state`呢?对!不管是setup store还是option store,Pinia都给你准备好了`$state`这个属性,它是整个store内部state的响应式代理对象的“快捷访问器”,直接传`() => userStore.$state`,哦不对,或者直接传`userStore.$state`的话,和直接传userStore的区别是什么? 直接传`userStore.$state`的话,watch默认监听的是整个$state对象的引用?不对,等下实际测试过setup store的话,直接传`() => userStore.$state`或者直接传`userStore.$state`,都可以?不过更稳妥的是用箭头函数包裹,或者直接传`() => ({ ...userStore.$state })`?哦不,等下不用展开,展开的话就是普通对象了,不对,直接传`userStore.$state`或者用箭头函数返回`userStore.$state`,都能监听内部state的变化,但这个时候如果只需要加deep:true吗?对,因为不管内部哪个字段变了,整个$state对象本身的引用没变,所以要加deep:true。 那什么时候用这个方法呢?比如后台里的全局设置页,有很多个设置项,比如主题颜色、字体大小、是否开启暗黑模式,只要有任意一个设置项变了,都要存到localStorage或者sessionStorage里,这个时候一个个写箭头函数太麻烦了,就可以监听整个$state,或者监听设置页对应的store的整个$state。 举个存全局设置到localStorage的例子: ```javascript import { watch } from 'vue'; import { useAppStore } from '@/stores/app'; export default { setup() { const appStore = useAppStore(); watch( () => appStore.$state, (newState) => { // 存到localStorage里 localStorage.setItem('appSettings', JSON.stringify(newState)); }, { deep: true, immediate: true // 首次加载组件时也会存一次,或者初始化的时候从localStorage里取 } ); // 顺便加个初始化的逻辑,从localStorage里取设置: onMounted(() => { const savedSettings = localStorage.getItem('appSettings'); if (savedSettings) { // 初始化的时候可以用$patch直接替换,不管是setup还是option store都可以用$patch appStore.$patch(JSON.parse(savedSettings)); } }); return {}; } } ``` 对了,这里顺便提一下,初始化的时候用$patch比一个个赋值更高效,也更符合Pinia的规范,这个也是个小技巧。 ### 必须要注意的3个避坑指南,别踩! 刚才说了方法,但是避坑更重要,不然写了半天要么没效果要么性能炸锅,这3个坑我和身边的朋友都踩过。 #### 坑一:监听整个setup store或者不加箭头函数不加toRef/toRefs不加$state 这个刚才已经说过很多次了,但还是有人踩,尤其是刚转Pinia的朋友,再强调一遍: - 不能直接传整个store实例,除非你要监听整个store的引用变化(基本没人会); - 不能普通解构后直接传基本数据类型的字段; - 要精准的话用方法一,要v-model绑定的话用方法二,要批量监听存本地的话用方法三。 #### 坑二:滥用deep:true 很多人不管三七二十一,不管监听什么都加deep:true,比如监听单个基本数据类型的字段也加,这个是完全没必要的,还会降低性能! 什么时候才加deep:true? - 只有当你监听的是**深层嵌套的对象或数组**的时候才需要加,比如监听userStore.userInfo,userInfo里有address,address里有province,你要监听province的变化,这个时候才需要加; - 或者用方法三监听整个$state的时候才需要加。 怎么避免滥用deep:true? - 可以用方法一的箭头函数精确到最深层的那个字段,() => userStore.userInfo.address.province`,这个时候就不需要加deep:true了,性能会提升很多! 比如刚才的实时校验userInfo里的address.province的时候,就可以直接监听最深层的province,不用加deep:true,太爽了。 #### 坑三:忘记用immediate,导致刷新页面或者首次加载组件时不触发 很多场景都是需要首次加载组件时或者刷新页面时触发的,比如刚才的重新请求侧边栏目录、存全局设置、初始化全局设置这些,如果忘记加immediate,刷新页面后侧边栏就不会出来,全局设置也不会初始化,这个也是个高频踩坑点。 什么时候加immediate? - 只要你需要首次加载组件时或者刷新页面时触发回调,就加immediate:true,比如刚才的场景都要加。 ### 好了,今天的内容就讲完了, - 最常用、性能最优的是方法一:用箭头函数/getter包裹单个/多个字段; - 要v-model绑定的话用方法二:用toRefs/toRef解构后直接传; - 要批量监听存本地的话用方法三:监听整个$state,加deep:true和immediate:true; - 避坑指南要注意:不能随便传整个store、不能滥用deep:true、不能忘记用immediate。 还有什么问题吗?可以在评论区留言哦!

版权声明

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

热门