掘金文章归档 · 第 19 篇 Vue3 / Vite 系列 2/4 状态管理

Vue3 组合式函数:能否作为模块级全局状态管理?

模块作用域闭包缓存 + 响应式系统独立性,3 个递进示例 + 与 React Hook 对比

原文作者:随意_(掘金) | juejin.cn/post/7575718948299112499 | 发布于 2025-11-24 | 阅读 284 · 约 15 分钟

0先明确结论

在 Vue3 生态中,组合式 API(Composition API)的出现彻底改变了逻辑复用的方式,其中组合式函数(Composables)作为核心载体,不仅能复用组件逻辑,也常被开发者尝试用于全局状态管理。

📌 三个结论:
  1. Vue3 组合式函数可以实现模块级全局状态管理,且相比 React 自定义 Hook 更"天然"——因为 Vue3 的响应式系统(Proxy)不依赖组件实例,模块级响应式状态可直接被跨组件共享;
  2. 核心原理是模块作用域 + 响应式状态的闭包缓存:模块初始化时创建的响应式对象(ref/reactive)被存储在模块闭包中,所有组件导入并调用组合式函数时共享同一个响应式实例;
  3. 每次组件引入并调用组合式函数时,内部的响应式状态不会重新初始化——仅函数逻辑会重复执行,但始终返回模块缓存的同一个响应式对象。

1为什么 Vue3 组合式函数更适合做全局状态

1.1 模块作用域:状态的"全局容器"(与 React 一致)

和 ES6 模块机制相同,Vue3 中每个 .js / .vue 文件都是独立模块:模块被首次导入时执行一次初始化逻辑,创建内部变量的实例;后续所有组件导入该模块时,共享的是同一个模块实例,模块内的变量不会被重复创建——这是全局状态能"共享"的基础。

1.2 Vue3 响应式系统:不依赖组件实例(核心差异点)

📌 简单说:Vue3 模块内创建的响应式状态,本身就是"全局可用"的,组合式函数只是封装了状态和操作方法,让组件能便捷地访问和修改。

1.3 组合式函数的"全局化"本质:闭包缓存响应式实例

核心逻辑:在模块内创建响应式状态(ref/reactive)→ 封装修改方法(如 setUser/logout)→ 组合式函数执行时直接返回模块缓存的响应式状态和方法 → 所有组件拿到的是同一个响应式实例,状态变更同步到所有使用该状态的组件。

✅ 对比 React:Vue3 无需手动维护订阅者(subscribers 集合),因为响应式对象本身会触发依赖追踪,组件使用状态时自动成为依赖,状态更新时自动触发组件重新渲染。

2实战示例:3 个递进实现

示例 1:基础版全局状态(共享用户信息)

// useGlobalUser.js(模块级全局状态组合式函数)
import { ref, computed } from 'vue';

// 模块作用域的响应式状态:闭包缓存,仅初始化一次
const user = ref(null); // 全局用户状态(ref 确保基本类型响应式)

// 计算属性:派生状态(是否登录)
const isLogin = computed(() => !!user.value);

// 操作方法:修改全局状态
const login = (userInfo) => {
  // 替换响应式对象的值(ref 通过 .value 访问/修改)
  user.value = { ...userInfo };
};

const logout = () => {
  user.value = null;
};

// 组合式函数:供组件调用
export function useGlobalUser() {
  // 直接返回模块缓存的响应式状态和方法(所有组件共享同一实例)
  return {
    user,
    isLogin,
    login,
    logout
  };
}
<!-- Header.vue 组件 -->
<template>
  <div class="header">
    <h1>Vue3 全局状态示例</h1>
    <div class="user-info">
      <span v-if="isLogin">欢迎 {{ user.name }} </span>
      <span v-else>未登录</span>
      <button v-if="isLogin" @click="logout">登出</button>
    </div>
  </div>
</template>

<script setup>
// 导入并调用组合式函数
import { useGlobalUser } from './useGlobalUser';
const { user, isLogin, logout } = useGlobalUser();

// 验证状态是否共享:组件渲染时打印
console.log('Header 组件渲染:', user.value);
</script>
<!-- Login.vue 组件 -->
<template>
  <div class="login">
    <button @click="handleLogin">模拟登录(张三)</button>
  </div>
</template>

<script setup>
import { useGlobalUser } from './useGlobalUser';
const { login } = useGlobalUser();

const handleLogin = () => {
  // 模拟接口返回的用户信息
  login({ id: 1, name: '张三', avatar: 'xxx.png' });
};
</script>
<!-- App.vue 根组件 -->
<template>
  <Header />
  <Login />
</template>

<script setup>
import Header from './Header.vue';
import Login from './Login.vue';
</script>
✅ 效果验证:初始 Header 显示"未登录";点击"模拟登录"→ Login 修改全局 user → Header 因依赖 user 和 isLogin 自动重渲染显示"欢迎张三";点击"登出"→ 自动渲染"未登录"。核心亮点:无需手动维护订阅者,响应式系统自动追踪依赖;代码极简,省去 React 版的 notifySubscribers 和 forceUpdate 逻辑。

示例 2:优化版——支持初始化配置与状态隔离

// useGlobalState.js(通用全局状态组合式函数)
import { ref, reactive, isRef, unref } from 'vue';

// 模块缓存:存储不同 key 对应的全局状态
const globalStateMap = new Map();
// 模块缓存:存储不同 key 对应的初始化配置
const initConfigMap = new Map();

/**
 * 通用全局状态组合式函数
 * @param {string} key - 状态唯一标识(用于隔离不同状态)
 * @param {any} initialValue - 初始值(支持普通值或响应式对象)
 * @returns {[ref/reactive, function]} - [状态, 更新函数]
 */
export function useGlobalState(key, initialValue) {
  // 若状态已存在,直接返回缓存的实例
  if (globalStateMap.has(key)) {
    return globalStateMap.get(key);
  }

  // 初始化响应式状态:根据初始值类型选择 ref 或 reactive
  const state = isRef(initialValue)
    ? initialValue
    : (typeof initialValue === 'object' && initialValue !== null)
      ? reactive(initialValue)
      : ref(initialValue);

  // 保存初始化配置(便于后续重置)
  initConfigMap.set(key, initialValue);

  // 更新函数:支持直接赋值或函数式更新
  const setState = (updater) => {
    if (typeof updater === 'function') {
      // 函数式更新:接收当前状态,返回新状态
      const newValue = updater(unref(state));
      Object.assign(state, newValue); // reactive 直接合并
      // 若为 ref,直接赋值:state.value = newValue
      if (isRef(state)) state.value = newValue;
    } else {
      // 直接赋值
      Object.assign(state, updater);
      if (isRef(state)) state.value = updater;
    }
  };

  // 重置函数:恢复到初始值
  const resetState = () => {
    const initialValue = initConfigMap.get(key);
    setState(initialValue);
  };

  // 缓存状态和方法(key 作为唯一标识)
  const result = [state, setState, resetState];
  globalStateMap.set(key, result);

  return result;
}
<!-- 组件A:使用用户状态 -->
<script setup>
import { useGlobalState } from './useGlobalState';

// 初始化用户状态(key: 'user',初始值:null)
const [user, setUser, resetUser] = useGlobalState('user', null);

// 直接赋值更新
const handleLogin = () => {
  setUser({ id: 1, name: '张三' });
};

// 函数式更新(依赖当前状态)
const updateName = () => {
  setUser(prev => ({ ...prev, name: '张三-更新' }));
};

// 重置到初始值(null)
const handleReset = () => {
  resetUser();
};
</script>
<!-- 组件B:使用主题状态(与用户状态隔离) -->
<script setup>
import { useGlobalState } from './useGlobalState';

// 初始化主题状态(key: 'theme',初始值:{ mode: 'light' })
const [theme, setTheme] = useGlobalState('theme', { mode: 'light' });

const toggleTheme = () => {
  setTheme(prev => ({ mode: prev.mode === 'light' ? 'dark' : 'light' }));
};
</script>
<!-- 组件C:共享用户状态(与组件A共用同一个 state) -->
<script setup>
import { useGlobalState } from './useGlobalState';

// 无需重新初始化,直接获取 key: 'user' 的缓存状态
const [user] = useGlobalState('user');
console.log('组件C共享的用户状态:', user.value); // 与组件A的 user 完全一致
</script>
📌 优化点:支持多状态隔离(key 区分 user/theme);自适应响应式类型(按初始值自动选 ref/reactive);支持函数式更新和重置,便于状态回溯。

示例 3:进阶版——支持异步状态与依赖注入

// useGlobalAuth.js(异步+依赖注入的全局状态)
import { ref, computed, provide, inject, onMounted } from 'vue';

// 注入标识(避免命名冲突)
const GLOBAL_AUTH_KEY = Symbol('GLOBAL_AUTH');

// 模块级响应式状态(异步初始化)
const user = ref(null);
const loading = ref(true); // 加载状态
const error = ref(null); // 错误状态
const isLogin = computed(() => !!user.value);

// 异步初始化:从接口获取用户信息(如刷新页面后恢复登录状态)
const fetchUserInfo = async () => {
  try {
    loading.value = true;
    // 模拟接口请求
    const res = await fetch('/api/user/info');
    const data = await res.json();
    user.value = data;
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};

// 操作方法
const login = async (username, password) => {
  loading.value = true;
  try {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ username, password })
    });
    const data = await res.json();
    user.value = data.user;
    return data;
  } catch (err) {
    error.value = err.message;
    throw err;
  } finally {
    loading.value = false;
  }
};

const logout = async () => {
  await fetch('/api/logout');
  user.value = null;
};

// 父组件提供全局状态(如 App.vue 中使用)
export function provideGlobalAuth() {
  // 组件挂载时初始化用户信息
  onMounted(() => {
    fetchUserInfo();
  });

  // 提供状态和方法(子组件通过 inject 获取)
  provide(GLOBAL_AUTH_KEY, {
    user,
    isLogin,
    loading,
    error,
    login,
    logout
  });
}

// 子组件注入全局状态
export function useGlobalAuth() {
  const auth = inject(GLOBAL_AUTH_KEY);
  if (!auth) {
    throw new Error('useGlobalAuth 必须在 provideGlobalAuth 的后代组件中使用');
  }
  return auth;
}
<!-- App.vue(根组件提供状态) -->
<script setup>
import { provideGlobalAuth } from './useGlobalAuth';
// 提供全局状态(仅需在根组件调用一次)
provideGlobalAuth();
</script>

<template>
  <router-view /> <!-- 所有子组件均可注入使用 -->
</template>
<!-- 深层子组件(无需导入,直接注入) -->
<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误:{{ error }}</div>
    <div v-else>
      <span v-if="isLogin">欢迎 {{ user.name }}</span>
      <button v-else @click="handleLogin">登录</button>
    </div>
  </div>
</template>

<script setup>
import { useGlobalAuth } from './useGlobalAuth';
const { user, isLogin, loading, error, login } = useGlobalAuth();

const handleLogin = async () => {
  await login('admin', '123456');
};
</script>
📌 进阶特性:异步状态管理(内置 loading/error 状态,适配接口请求场景);依赖注入(避免深层组件重复导入组合式函数);模块级缓存(异步请求仅执行一次,所有组件共享请求结果)。

3关键疑问:状态会重新初始化吗?

3.1 结论:不会重新初始化,仅函数逻辑重复执行

3.2 代码验证:打印日志看执行次数

// useGlobalUser.js
import { ref, computed } from 'vue';

console.log('模块初始化:仅执行一次'); // 模块首次导入时打印

// 模块级响应式状态(仅初始化一次)
const user = ref(null);
console.log('响应式状态初始化:仅执行一次'); // 模块首次导入时打印

const isLogin = computed(() => !!user.value);

export function useGlobalUser() {
  console.log('组合式函数执行:组件调用时执行'); // 组件每次调用都会打印
  return { user, isLogin };
}
时机日志输出
应用启动时(模块首次导入)模块初始化:仅执行一次 → 响应式状态初始化:仅执行一次
第一个组件(Header)调用组合式函数执行:组件调用时执行
第二个组件(Login)调用组合式函数执行:组件调用时执行(无"响应式状态初始化"日志)
组件重渲染时(如登录后)两个组件都打印"组合式函数执行"(仍无"响应式状态初始化"日志)

3.3 与 Vue3 局部组合式函数的区别

特性局部组合式函数(useLocalCounter)全局组合式函数(useGlobalUser)
状态存储位置组件实例的 setup 作用域中模块作用域的闭包中
状态共享性组件私有(每个组件调用创建独立状态)跨组件共享(所有组件调用共享同一状态)
响应式状态初始化次数每个组件调用一次模块初始化时仅一次
函数执行次数组件每次渲染执行组件每次调用执行(仅返回缓存)

4Vue3 组合式函数 vs React 自定义 Hook

对比维度Vue3 组合式函数React 自定义 Hook
响应式基础基于 Proxy 的独立响应式系统(不依赖组件)基于组件 Hook 链表(依赖组件实例)
订阅机制自动依赖追踪(无需手动维护订阅者)需手动维护订阅者或 forceUpdate
代码简洁度极简(直接返回响应式对象)需额外处理订阅和更新触发
状态类型支持自动适配 ref(基本类型)/ reactive(对象)统一用 useState(需手动浅拷贝对象)
SSR 兼容性需额外处理(模块状态共享给所有请求)同样需额外处理(状态污染问题)
📌 核心差异:Vue3 的响应式系统"脱离"组件实例,使模块级全局状态的实现更自然、代码更简洁;而 React 需通过闭包"脱离"组件 Hook 链表,且需手动处理组件重渲染触发。

5适用场景与局限性 & 总结

5.1 适用场景

5.2 局限性

局限说明
SSR 状态污染风险模块级状态在 SSR 中被所有请求共享,需通过 serverPrefetch 或状态隔离机制解决
缺乏状态调试工具没有 Pinia DevTools 那样的可视化调试工具,难以追踪状态变更历史
复杂状态逻辑维护成本高状态依赖、批量更新、异步流控制(如请求取消)需手动实现
状态持久化需手动处理刷新页面后状态丢失,需手动结合 localStorage/sessionStorage
多团队协作冲突风险无统一命名规范时可能出现 key 冲突(如多团队都用 user 作 key)
✅ 总结:Vue3 组合式函数实现模块级全局状态的核心是模块作用域的闭包缓存 + 响应式系统的独立性。响应式状态不会重新初始化(仅模块首次导入时执行一次),函数逻辑重复执行仅为返回缓存实例,无性能开销。若项目需复杂状态管理(多模块状态、调试、SSR 兼容),推荐使用 Vue 官方推荐的 Pinia(本质是基于组合式函数 + 模块作用域实现的成熟方案)。