发布订阅:用 pinia 实现全局无缝通信

前言

细数 vue 中的数据通信方式,足足有十几种,其中包括了父子互传、兄弟互传、隔代互传、无关互传,这十几种方式每个都有其不同的语法,也就是说我们需要根据这些组件的亲戚关系选择最合适的通信方式,我们无时无刻不在使用各种形式的参数传递。MD,像极了我们混乱的前端框架圈。

mqtt 协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布,适用于 低性能 的物联网设备。

为什么要提mqtt呢,因为我接下来要说的和mqtt的通信方式息息相关。

首先简单介绍下 mqtt 这个东西,简单来说,它由三个部分组成:服务器(mqtt broker)、发布者(publisher)、订阅者(subscriber)。其中发布者和订阅者的身份通常是混合的,即一个用户可以既是发布者又是订阅者,当发布消息时,就是发布者,当订阅时,就是订阅者。

在发布消息时,需要指定一个主题(topic),当然,订阅时也要指定一个主题。向某个主题发布消息时,服务器会向所有订阅这个主题的客户端分发消息,当订阅者接收到时,再根据消息内容进行它的下一步操作。

举个例子,大概是:你(订阅者)跟你妈(服务器)说,你弟弟(发布者)打游戏就告诉你,你好打他,然后你妈说好的(订阅成功),然后你弟弟打游戏(发布)的一瞬间,消息被你妈捕获到了,然后告你了,你就成功打了你弟弟。

在上面的例子中,你通过订阅消息,而后执行了某些操作

发布订阅思想

我们都知道,vue的双向绑定原理是基于发布订阅范式的(面试必备,擦,哥们)。想必你和我一样,即使看了几遍 vue 双向绑定原理,还是对这个字眼不太理解,什么叫发布订阅范式,发布订阅在生活和编程中无处不在,是一种通俗意义,并不是编程专有术语。

其实,vue 中的 watch / watchEffect 就是一个订阅的表现。

scss 复制代码
watch(someValue,()=>{
    // ... 
})

上面的代码,订阅了 someValue值变化这一事件。但是好像这个例子中缺失了发布者,其实发布者是隐式存在的。

ini 复制代码
someValue = otherValue

此时,这个js 语句就成了 发布者。

vue3 中的 watch 方法十分强大,它支持 深度监听、监听对象、对象下的单个属性、多变量监听等。

使用 watch 和 pinia 实现全局事件发布订阅

通过上面的描述,不难发现用 watch 来监听 pinia 中的某些属性,即可达到订阅的效果,从而实现无障碍 传参/通信。

以这种方式传参,完全不用考虑值从哪里来,将要到哪里去。哪里用,就在哪里 watch。

适用场景

通过监听全局变量变化从而执行下一步操作的这种写法,听起来好像对性能影响很大,而实际上呢,pinia 对此已经做了比较合适的机制,首先 Pinia store 依靠 pinia 实例在所有调用中共享同一个 store 实例,你可以定义任意多的 store,而这些 store 不会单独占用内存,所以放心大胆的在多文件中创建 store 即可。

比如,现在有一个 全局的地图组件mapbox,又有若干个小组件控制着地图上的图层的增删改查,我们无需在每个组件中都向mapbox组件传值、或者用ref获取其实例,我们只需把需要的数据放到pinia中,然后在mapbox组件内对其监听。

scss 复制代码
// Mapbox.vue
watch(() => store.currGridData, (newValue) => {
  renderGrid(newValue)
})

上面的代码中监听了store.currGridData数据,当其数据有变化时,则重新渲染此数据。当然,如果需要清除,就直接把此数据置空就好了,可以在监听中新增空数据处理,或者在renderGrid函数中处理。

这么一来,所有的组件,无论在什么位置,什么路径,只要监听某值的变化,即可做到类似传值的效果。

潜在的问题

内存泄露

当然世界上没有十全十美的事情,这么干正常情况下对性能几乎是没有影响的,但一旦出现问题,那将是致命的。虽然 pinia 的实例是单例的,但是引用却不是,每新增一个 对store值的watch,就会新增一个对此值的引用,正常情况下,vue会主动在页面销毁时移除这些 watch,但是如果 watch 放在了异步体内,则需要注意手动移除它们,否则就会使这些内存常驻,也就是内存泄露。

死循环

完全依靠 watch 来执行操作,会在不经意间造成死循环或者栈溢出的问题。举个例子

scss 复制代码
watch(()=>store.someValue,()=>{
    store.someValue = otherValue
})

如果otherValue的值是一个固定值,那么在下一次将不再进入了,因为值没有再变化,但是如果值不是固定值,很容易看出来,这个 watch 实际上是一个死循环,当操作了store.someValue的值后,又会重新回到这个watch。要避免这个问题也很简单,像通常的递归函数一样,给他一个跳出的条件即可

scss 复制代码
watch(()=>store.someValue,(nv)=>{
    if(nv == someValue) return
    store.someValue = otherValue
})
不触发问题

当监听的某值不变化时,watch 不会触发,这样就会导致另一个问题,如果依赖某个值的变化来做一些操作,而这个操作和这个值的变化又不是双向耦合的,此时就会造成给某值赋值时,不会触发 watch。

例如通过值goNewArea来使地图跳转到新的地区视角。

scss 复制代码
watch(()=>store.goNewArea,()=>{
    map.flyTo({
    // ...
    })
})

比如有一个按钮,会将其赋值为北京市,那么地图正常跳转到北京,如果此时我们通过拉动地图视角,使实际区域变成了上海市,此时再通过按钮将其赋值为北京市时,地图将不会触发跳转,因为store.goNewArea没有变化。

要解决这个问题,则应该给这个值一个置空。即:

javascript 复制代码
watch(()=>store.goNewArea,()=>{
    map.flyTo({
    // ...
    })
    map.once('moveend',()=>{
        store.goNewArea = ""
    })
})

上面提到,这样做会导致watch重复触发死循环的问题,所以还要再加一句:

javascript 复制代码
watch(()=>store.goNewArea,(nv)=>{
    if(nv == "") return
    map.flyTo({
    // ...
    })
    map.once('moveend',()=>{
        store.goNewArea = ""
    })
})

总结

使用 watch 配合 pinia 全局变量,来实现全局的事件控制,在一定意义上简化了开发流程,说实话,写起来非常爽,也非常清晰。但是一旦变量多起来,相互影响的概论就会增大,管理庞大的 watch 们也是一项负担,你需要时刻注意不要内存溢出、死循环。

总之这种写法的灵活性拉满了,但是需要非常清晰的思路和对业务深刻的理解,才能游刃有余。

相关推荐
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
2401_857600956 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600956 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL6 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js
轻口味6 小时前
Vue.js 核心概念:模板、指令、数据绑定
vue.js
2402_857583496 小时前
基于 SSM 框架的 Vue 电脑测评系统:照亮电脑品质之路
前端·javascript·vue.js
java_heartLake7 小时前
Vue3之性能优化
javascript·vue.js·性能优化
ddd君317748 小时前
组件的声明、创建、渲染
vue.js
前端没钱8 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
顽疲9 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端