Vue 3 使用全局事件的另一种方案

Vue 3 使用全局事件的另一种方案

zeee 168 2024-08-08

问题背景

在 Vue3 中删除了全局事件总线的设计: $on$off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口。如果向直接通过 Vue 实例触发事件, 并在其他组件进行监听, 在核心 Vue 库里面是无法实现的。

原因:

在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。

现有方案

官方文档 里面,Vue 建议两个方案(具体的实现代码可以在文档中查看,这里便不再重复):

  1. 根组件注册:即在根组件中注册监听, 那么在任意的子组件内通过 emit 触发的事件都可以在根组件处得到捕获处理。

  2. 使用第三方事件总线库, 例如 mitt 或 tiny-emitter

方案一的问题是处理场景具有局限性, 如果需要处理的内容刚好可在根组件完成,那么这个很好用, 而没有任何额外成本, 但是更多时候希望在某个子组件内完成时, 方案一就无能为力了。

方案二看起来是最佳的替代方案,因为使用第三方库本身就是专注于事件总线本身。它的问题只是会引入新的库, 以及随之而来的学习成本。 如果项目中大量需要事件总线, 那么这个方法毋庸置疑非常合适。 但是如果只有少数几个地方需要, 或者项目对第三方库的引用比较严格, 那么或许可以寻找另一种"平替方案"

其他方案思考:

在官网最后有提到, 使用全局状态管理例如 Pinia 可以实现类似的效果, 但是没有继续展开, 这里结合 Pinia 的文档简单做一个思路的整理和实现思考:

  1. 监听State
    因为 Pinia 的状态可以定义成响应式的, 那么便可以通过监听 State 来实现类似全局事件的效果:

    • State 中定义变量 S1

    • 通过 Watch 监听 S1 变化,进行相关处理

    • 通过调用 Set 方法变更 S1 触发相关的监听方法。

    这个方法除了可以是实现零类似全局事件的效果之外, 还有另一个好处,就是 S1 本身便可以作为时间参数进行存储,这样便不需要再额外的定义,大概实现的伪代码如下:

    // state.js
    import { defineStore } from 'pinia'
    
    const useEventBusStore = defineStore('eventBus', {
      state: () => {
        return {
            someEventParams: 0
        }
      },
    })
    
    // 组件
    // 1. 触发
    const eventBus = useEventBusStore ()
    eventBus.someEventParams = 1;
    
    // 2. 监听
    watch(eventBus.someEventParams, (new) => {
       // 事件处理逻辑
    })
    
    
    

    其实 Pinia 便提供了 State 的监听接口, 即 $subscribe , 道理是类似的, 不过除了订阅 状态, Pinia 还提供了对 Action 的监听,也就是下面要讲的方法 这种方法看起里就更像是触发事件了。

  2. 监听 Action

    你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。

    以上是 Pinia 对订阅 Action的描述, 这个方法对函数运行追踪很有用。 自然地也可以利用这个特性, 来实现类似事件的效果, $onAction 的定义如下:

    const unsubscribe = someStore.$onAction(
      ({
        name, // action 名称
        store, // store 实例,类似 `someStore`
        args, // 传递给 action 的参数数组
        after, // 在 action 返回或解决后的钩子
        onError, // action 抛出或拒绝的钩子
    }) => {
        // 回调函数,实际上在方法调用前执行
    })
    
    // 调用unsubscribe可以手动删除监听器
    unsubscribe()
    
    

    使用时需要注意的是:$onAction 的回调函数是在方法执行前执行的, 所以如果监听的结果需要再调用结束后完成的话, 监听的逻辑并不应该写在回调函数中,而应该参数中的 after 钩子内实现:

    const unsubscribe = someStore.$onAction(options => {
        options.after(result => {
            // 先监听逻辑
        })
    })
    

    这个方法的好处如下:

    第一可以通过方法的规范命名(如 onSomething) 实现更加符合事件使用习惯的代码,增加可读性。

    其次,通过方法返回值可以带出事件参数,无需从其他处去取。

    另外,可以通过调用$onACtion 的返回值手动移除监听, 使用起来更加灵活。

总结:

在大多数场景中,我们都会使用全局状态组件如Pinia. 但是对事件总线的依赖,或许只是一个或两个方法, 那么此时再引入一个事件总线的库, 也许并不是一个最佳的方案, 就如官网所说, 如果可以在根组件进行处理, 那么直接在 CreateApp 处实现一个监听方法即可, 而如果需要在组件内监听的话, 也可以使用对全局状态的监听或状态组件如Pinia自己提供的一些特性来实现相关功能。在降低项目复杂度的同时,通过合理的代码组织甚至实现效果并不比第三方库差太多。


参考链接:


# vue # pinia