Skip to content

Vue 全局状态管理:从零实现 defineStore

这份笔记将带你了解如何不依赖 Pinia 或 Vuex 等第三方库,手动实现一个轻量级的全局状态管理方案。这在面试或需要严格控制项目体积的场景中非常有用。

核心目标

创建一个 defineStore 函数,它能生成一个类似于 Pinia useStore 的 Hook 函数。无论在哪个组件中调用这个 Hook,都应返回同一个状态实例,从而实现全局共享和响应式。

实现思路一:利用闭包缓存状态

这是最核心、最基础的实现方式,巧妙地利用了 JavaScript 的闭包(Closure)特性。

1. 基本结构

defineStore 函数接收一个“设置函数” (setupfn)作为参数,这个函数返回一个包含 stategettersactions 的对象。defineStore 本身则返回另一个函数,我们称之为 useStore

javascript
function defineStore(setup) {
  // `useStore` 就是最终在组件中使用的那个 Hook
  return function useStore() {
    // ... 核心逻辑在这里
  }
}

2. 问题的关键

如果 useStore 每次被调用时,都重新执行 setup() 函数,那么每个组件拿到的都会是一个全新的状态对象,无法实现共享。

3. 解决方案:闭包 + 单例模式

我们在 defineStore 的作用域内(但在返回的 useStore 函数外)声明一个变量 state,用于缓存 setup() 的执行结果。

  • 首次调用 useStore 时,state 为空。此时,执行 setup() 函数,将其返回值(即状态对象)赋给 state,然后返回 state
  • 后续调用 useStore 时,state 已经有值了。此时,直接返回缓存的 state,不再执行 setup()

这样,无论调用多少次 useStore,得到的都是第一次创建的那个唯一的 state 对象。

代码实现:

javascript
import { ref, reactive } from 'vue'; // 确保状态是响应式的

/**
 * 定义一个 store
 * @param {Function} setup 返回状态对象的函数
 * @returns {Function} useStore Hook
 */
function defineStore(setup) {
  let state; // 用于缓存状态的变量,形成闭包

  // 这个函数就是我们在组件中使用的 useStore()
  return function useStore() {
    // 仅在第一次调用时初始化 state
    if (!state) {
      state = setup();
    }
    // 后续所有调用都返回第一次创建的 state 实例
    return state;
  };
}

// ---- 使用示例 ----

// 1. 定义 store (stores/counter.js)
export const useCounterStore = defineStore(() => {
  const count = ref(0);
  function increment() {
    count.value++;
  }
  return { count, increment };
});

// 2. 在组件中使用
// import { useCounterStore } from './stores/counter';
// const counterStore = useCounterStore();
// counterStore.increment();

重点小结: 该方法的核心是利用闭包将 state 变量“锁”在了 defineStore 的作用域内,使其在多次调用 useStore 之间得以存活和共享,从而实现了单例模式。


实现思路二:使用 effectScope (进阶)

这是一种更健壮、更符合 Vue 3 设计理念的实现方式。它能更好地管理响应式副作用(如 computed, watch 等)的生命周期。

1. effectScope 是什么?

effectScope 是 Vue 3 提供的一个高级 API。它可以创建一个“作用域”,所有在该作用域内创建的响应式副作用(computed, watchEffect 等)都会被收集起来。当这个作用域被停止 (scope.stop()) 时,所有被收集的副作用也会被一并停止,有效防止内存泄漏。这对于库的作者来说尤其重要。

2. 改造 defineStore

我们可以用 effectScope 来包裹状态的创建过程。

代码实现:

javascript
import { effectScope } from 'vue';

function defineStore(setup) {
  let state;
  const scope = effectScope(); // 1. 创建一个 effect scope

  return function useStore() {
    if (!state) {
      // 2. 在 scope 内运行 setup 函数
      // 这样 setup 中所有的响应式 effect 都会被 scope 捕获
      state = scope.run(() => setup());
    }
    return state;
  };
}

3. 为什么这样更好?

  • 生命周期管理:如果你的 store 很复杂,包含 watchcomputedeffectScope 可以让你在未来有能力统一销毁这些副作用(例如,通过调用 scope.stop()),这在单页应用中切换不同业务模块、动态卸载 store 时非常有用。
  • 代码更专业:在面试中展示这种用法,能体现出你对 Vue 响应式系统有更深入的理解,而不仅仅停留在业务层面。

重点小结: effectScope 为 store 的响应式系统提供了一个独立的生命周期容器。虽然在简单场景下与闭包方法效果相同,但它为高级的内存管理和状态卸载提供了可能,是更工业化的解决方案。


最终总结

特性实现思路一 (闭包)实现思路二 (effectScope)
核心原理闭包缓存,实现单例模式effectScope 捕获副作用,闭包缓存
优点简单易懂,纯 JS 特性,足以应对大部分场景专业,健壮,提供完整的响应式生命周期管理,防止内存泄漏
适用场景中小型项目、面试基础题、快速实现库开发、大型复杂应用、需要动态管理 store 的场景、面试进阶题