模块作用域闭包缓存 + 响应式系统独立性,3 个递进示例 + 与 React Hook 对比
在 Vue3 生态中,组合式 API(Composition API)的出现彻底改变了逻辑复用的方式,其中组合式函数(Composables)作为核心载体,不仅能复用组件逻辑,也常被开发者尝试用于全局状态管理。
和 ES6 模块机制相同,Vue3 中每个 .js / .vue 文件都是独立模块:模块被首次导入时执行一次初始化逻辑,创建内部变量的实例;后续所有组件导入该模块时,共享的是同一个模块实例,模块内的变量不会被重复创建——这是全局状态能"共享"的基础。
ref / reactive 独立于组件实例——只要创建了响应式对象,无论在模块内还是组件内,其响应式能力都由 Proxy 代理机制保证,无需依赖组件上下文。核心逻辑:在模块内创建响应式状态(ref/reactive)→ 封装修改方法(如 setUser/logout)→ 组合式函数执行时直接返回模块缓存的响应式状态和方法 → 所有组件拿到的是同一个响应式实例,状态变更同步到所有使用该状态的组件。
// 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>
// 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>
// 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>
// 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)调用 | 组合式函数执行:组件调用时执行(无"响应式状态初始化"日志) |
| 组件重渲染时(如登录后) | 两个组件都打印"组合式函数执行"(仍无"响应式状态初始化"日志) |
| 特性 | 局部组合式函数(useLocalCounter) | 全局组合式函数(useGlobalUser) |
|---|---|---|
| 状态存储位置 | 组件实例的 setup 作用域中 | 模块作用域的闭包中 |
| 状态共享性 | 组件私有(每个组件调用创建独立状态) | 跨组件共享(所有组件调用共享同一状态) |
| 响应式状态初始化次数 | 每个组件调用一次 | 模块初始化时仅一次 |
| 函数执行次数 | 组件每次渲染执行 | 组件每次调用执行(仅返回缓存) |
| 对比维度 | Vue3 组合式函数 | React 自定义 Hook |
|---|---|---|
| 响应式基础 | 基于 Proxy 的独立响应式系统(不依赖组件) | 基于组件 Hook 链表(依赖组件实例) |
| 订阅机制 | 自动依赖追踪(无需手动维护订阅者) | 需手动维护订阅者或 forceUpdate |
| 代码简洁度 | 极简(直接返回响应式对象) | 需额外处理订阅和更新触发 |
| 状态类型支持 | 自动适配 ref(基本类型)/ reactive(对象) | 统一用 useState(需手动浅拷贝对象) |
| SSR 兼容性 | 需额外处理(模块状态共享给所有请求) | 同样需额外处理(状态污染问题) |
| 局限 | 说明 |
|---|---|
| SSR 状态污染风险 | 模块级状态在 SSR 中被所有请求共享,需通过 serverPrefetch 或状态隔离机制解决 |
| 缺乏状态调试工具 | 没有 Pinia DevTools 那样的可视化调试工具,难以追踪状态变更历史 |
| 复杂状态逻辑维护成本高 | 状态依赖、批量更新、异步流控制(如请求取消)需手动实现 |
| 状态持久化需手动处理 | 刷新页面后状态丢失,需手动结合 localStorage/sessionStorage |
| 多团队协作冲突风险 | 无统一命名规范时可能出现 key 冲突(如多团队都用 user 作 key) |