前言
细数 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 们也是一项负担,你需要时刻注意不要内存溢出、死循环。
总之这种写法的灵活性拉满了,但是需要非常清晰的思路和对业务深刻的理解,才能游刃有余。