在做项目的过程中,发现 VueUse 一个很鸡肋的 hook 🧐

19 阅读4分钟

背景

最近刚入职一家公司,在项目中发现了一个非常巧妙的展示弹窗的方式:
首先我们想想,不管一个弹窗长什么样子,它都会有一个控制显示的 visible,父组件传进来的 props,还有就是 弹窗组件本身,基于这一点我的这位同事把它们封装到一个 hook 当中,并且暴露一个 onToggleComponent 方法切换组件。然后在一个公共组件内调用这个 hook 拿到这些公共状态,再用一个动态组件把这个弹窗显示出来,并且绑定对应的状态与事件。
我还真没这样用过,可能是之前接触到的项目太小,一个页面内也就那几个弹窗,不像现在有十几个弹窗,如果不封装 hook,光这些弹窗的显示逻辑都要占几十行代码了,还都是重复的。不得不说,这种方式确实是妙!👍
细心的 jym 已经发现这其中涉及到一个问题:公共状态
那么在 vue 项目中,当我们有很多组件需要共享状态时,你能想到几种方式?

共享状态的几种常用方式

1、props、emit(父与子共享状态)
2、provide,inject(多组件)
3、使用状态库,比如:vuex、pinia
4、发布订阅
5、......

以上是常用的几种方式,但是这里需要共享状态的是一堆同级弹窗组件,并且同一时间只有一个弹窗显示,也就是同一时间只有一个弹窗组件会用到这些状态,我的这位同事使用的是 vueuse 中一个创建全局状态的 hook:createGlobalState
vueuse官方文档中对它的介绍:
image.png
然后我先了解了一下它的使用:

createGlobalState 的使用

详细请查看:vueuse.nodejs.cn/shared/crea…

import { createGlobalState } from '@vueuse/core'
// store.js
import { computed, ref } from 'vue'

export const useGlobalState = createGlobalState(
  () => {
    // state
    const count = ref(0)

    // getters
    const doubleCount = computed(() => count.value * 2)

    // actions
    function increment() {
      count.value++
    }

    return { count, doubleCount, increment }
  }
)

这样其他不同组件就可以通过调用 useGlobalState 获取 count, doubleCount, increment 这三个状态,实现了状态共享。
非常方便是不是,然后我就想去了解一下 vueuse 是怎么实现的。下面是源码:
image.png
从官网进入源码发现实现并不复杂,函数体只有十几行代码,他是在一个闭包内保存了这些状态并返回一个 hook 去获取它们。但是这里又引出了一个新的 api,就是 vue 的 effectScope

effectScope的介绍与使用

image.png

// 类型
function effectScope(detached?: boolean): EffectScope

interface EffectScope {
  run<T>(fn: () => T): T | undefined // 如果作用域不活跃就为 undefined
  stop(): void
}
// 示例
const scope = effectScope()

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// 处理掉当前作用域内的所有 effect
scope.stop()

effectScope 函数会返回一个作用域对象,对象上有一个 run 方法和一个 stop 方法,run 方法接受一个状态工厂函数,它会统一收集内部产生的依赖到一个作用域内。调用 stop 函数处理掉当前作用域内的所有 effect,停止追踪状态变化。
官方文档:cn.vuejs.org/api/reactiv…
更详细的讲解:github.com/vuejs/rfcs/…

思考

effectScope 为我们提供了手动统一收集依赖,并且统一停止追踪依赖变化的语法糖。
vueuse 利用 vue 的 effectScope 统一收集了依赖,但是通过 createGlobalState 的源码来看他并没有调用stop函数去停止追踪依赖变化。
那既然你不需要统一停止追踪,那统一收集到一个作用域又有什么意义?直接使用一个简单闭包不是也能实现相同的效果,以下是闭包与effectScope两种实现方式的对比:

createGlobalState(vueuse源码)

type AnyFn = (...args: any[]) => any
export function createGlobalState<Fn extends AnyFn>(stateFactory: Fn): Fn {
  let initialized = false
  let state: any
  const scope = effectScope(true)

  return ((...args: any[]) => {
    if (!initialized) {
      state = scope.run(() => stateFactory(...args))!
      initialized = true
    }
    return state
  }) as Fn
}

myCreateGlobalState(闭包实现)

function myCreateGlobalState<Fn extends AnyFn>(stateFactory: Fn): Fn {
  const state = stateFactory()
  return (() => state) as Fn
}

两种实现的使用示例

// vueuse createGlobalState
export const useGlobalState = createGlobalState(
  () => {
    // state
    const count = ref(0)

    // getters
    const doubleCount = computed(() => count.value * 2)

    // actions
    function increment() {
      count.value++
    }

    return { count, doubleCount, increment }
  }
)

// 闭包方式
export const useGlobalState = myCreateGlobalState(
  () => {
    // state
    const count = ref(0)

    // getters
    const doubleCount = computed(() => count.value * 2)

    // actions
    function increment() {
      count.value++
    }

    return { count, doubleCount, increment }
  }
)

并且我在公司项目中把 vueuse 的 createGlobalState 函数更换为我的 myCreateGlobalState,依然能正常使用,那我是不是可以认为 createGlobalState 就是个很鸡肋的 hook😛。

总结

vueuse 通过 vue 的 effectScope 实现了 createGlobalState,用来创建全局状态,方便多组件取用。
但是我觉得 effectScope 的意义在于统一停止追踪依赖变化。createGlobalState 是为了创建全局状态,并不需要停止追踪依赖。那就没必要使用 effectScope 去实现,简单闭包实现即可。
这是我的理解,也是我的疑惑,如果有大佬知道使用 effectScope 实现的好处或意义,请在评论区指教🙆‍♂️(抱拳)

参考文章

vueuse.nodejs.cn/shared/crea…
github.com/vueuse/vueu…
cn.vuejs.org/api/reactiv…
juejin.cn/post/741292…