通过VueUse createGlobalState源码学习Vue3共享组件状态

1,266 阅读3分钟

说明

createGlobalState: 在全局作用域中保留状态,以便被 vue 实例复用。

// store.js
import { createGlobalState } from "@vueuse/core";
import { ref } from "vue";

export const useGlobalState = createGlobalState(() => {
  const count = ref(0);
  return { count };
});
// demo.vue
import { useGlobalState } from "./store";
const { count } = useGlobalState();

用于组件之间的状态共享。

源码

import { effectScope } from 'vue-demi'
import type { AnyFn } from '../utils'
/**
 * Keep states in the global scope to be reusable across Vue instances.
 *
 * @see https://vueuse.org/createGlobalState
 * @param stateFactory A factory function to create the state
 */
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
}

源码中使用了 effectScope 来声明 state。

创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。——vue 官网

思考

为什么使用 effectScope?

Vue3 组件状态共享可以使用 export const state = ref() 来实现。

// store.js
import { ref } from "vue";

export const count = ref(0);
// demo.vue
import { count } from './store'

那为什么 VueUse 要封装 createGlobalState 函数呢?为什么用 effectScope 来实现 createGlobalState 函数呢?

官网对 effectScope 描述很简单,并没有详细说明他的使用场景,能解决什么问题。

RFC 上有对 effectScope 的详细说明:

vue 的 setup 中声明的响应会被收集起来,在组件卸载时会自动取消响应的追踪。在组件之外或作为独立包使用时,需要处理 computed 和 watch 的 effects。通过 effectScope 的 stop 方法可以 effectScope 函数参数中的所有 effects。

“组件之外或作为独立包使用”的场景没有细说。

createGlobalState 源码中并没有提供外部访问 scope 的 stop 方法,采用 effectScope 并不是为了在组件之外或作为独立包使用时统一处理 computed 和 watch 的 effects。

而是为了解决 export const state = ref() 的另一个问题,上面提到组件卸载时会自动取消响应的追踪,也就是会自动取消 computed 和 watch 的 effects,在 RFC 上也提到了这个问题。

// uesCount.js
let count: Ref<number>;
let double: ComputedRef<number>;
export function useCount() {
  if (!count) {
    count = ref(0);
    watch(count, () => {
      console.log("watch count");
    });
    double = computed(() => {
      return count.value * 2;
    });
  }
  return { count, double };
}
// demo1.vue
<script setup lang="ts">
  import { useCount } from "./uesCount";
  const { count, double } = useCount();
</script>
<template>
  <button type="button" @click="count++">count is {{ count }}</button>
  double is {{ double }}
</template>

demo1 和 demo2 都用到到 count、double。在两个组件都渲染时,其中一个组件卸载导致另一组件的 watch 和 double 失效。

VueUse 使用 effectScope 来实现全局状态就是为了解决这个问题。VueUse 的 createGlobalState 是对 export const state = ref() 的扩展。

VueUse createGlobalState 和 Pinia 有什么区别呢?

Pinia 官网介绍说明了他们的区别:

Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。 如果您熟悉 Composition API,您可能会认为您已经可以通过一个简单的 export const state = reactive({}). 这对于单页应用程序来说是正确的,但如果它是服务器端呈现的,会使您的应用程序暴露于安全漏洞。 但即使在小型单页应用程序中,您也可以从使用 Pinia 中获得很多好处:

  • dev-tools 支持
    • 跟踪动作、突变的时间线
    • Store 出现在使用它们的组件中
    • time travel 和 更容易的调试
  • 热模块更换
    • 在不重新加载页面的情况下修改您的 Store
    • 在开发时保持任何现有状态
  • 插件:使用插件扩展 Pinia 功能
  • 为 JS 用户提供适当的 TypeScript 支持或 autocompletion
  • 服务器端渲染支持

Pinia 在服务器端渲染、状态的追踪调试、热更新上更好些。