背景
最近刚入职一家公司,在项目中发现了一个非常巧妙的展示弹窗的方式:
首先我们想想,不管一个弹窗长什么样子,它都会有一个控制显示的 visible,父组件传进来的 props,还有就是 弹窗组件本身,基于这一点我的这位同事把它们封装到一个 hook 当中,并且暴露一个 onToggleComponent 方法切换组件。然后在一个公共组件内调用这个 hook 拿到这些公共状态,再用一个动态组件把这个弹窗显示出来,并且绑定对应的状态与事件。
我还真没这样用过,可能是之前接触到的项目太小,一个页面内也就那几个弹窗,不像现在有十几个弹窗,如果不封装 hook,光这些弹窗的显示逻辑都要占几十行代码了,还都是重复的。不得不说,这种方式确实是妙!👍
细心的 jym 已经发现这其中涉及到一个问题:公共状态
那么在 vue 项目中,当我们有很多组件需要共享状态时,你能想到几种方式?
共享状态的几种常用方式
1、props、emit(父与子共享状态)
2、provide,inject(多组件)
3、使用状态库,比如:vuex、pinia
4、发布订阅
5、......
以上是常用的几种方式,但是这里需要共享状态的是一堆同级弹窗组件,并且同一时间只有一个弹窗显示,也就是同一时间只有一个弹窗组件会用到这些状态,我的这位同事使用的是 vueuse 中一个创建全局状态的 hook:createGlobalState
vueuse官方文档中对它的介绍:
然后我先了解了一下它的使用:
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 是怎么实现的。下面是源码:
从官网进入源码发现实现并不复杂,函数体只有十几行代码,他是在一个闭包内保存了这些状态并返回一个 hook 去获取它们。但是这里又引出了一个新的 api,就是 vue 的 effectScope
effectScope的介绍与使用
// 类型
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…