模块级作用域闭包缓存原理拆解,3 个递进示例 + state 是否重复初始化验证
在 React 开发中,状态管理是核心需求之一。从简单的组件内 state,到 Context API、Redux 等全局状态方案,开发者一直在根据场景选择合适的方案。而自定义 Hook 作为 React 的核心特性,因其复用逻辑的能力,常常被开发者尝试用于全局状态管理。
在 ES6 模块系统中,每个 .js 文件都是一个独立的模块,模块内的变量、函数会形成模块级作用域——即模块被导入时,会先执行一次初始化,之后所有导入该模块的地方,共享的是同一个模块实例,模块内的变量不会被重复创建。
当我们在模块中定义一个自定义 Hook,并在 Hook 内部调用 useState 时,这个 useState 的状态并不会绑定到某个具体组件,而是绑定到"模块级闭包"中。因为:
| 特性 | 普通自定义 Hook(如 useCounter) | 全局状态自定义 Hook(如 useGlobalUser) |
|---|---|---|
| 状态存储位置 | 组件实例的 Hook 链表中 | 模块作用域的闭包中 |
| 状态共享性 | 组件私有(每个组件调用创建独立状态) | 跨组件共享(所有组件调用共享同一状态) |
| state 初始化次数 | 每个组件调用一次 | 模块初始化时仅一次 |
| Hook 逻辑执行次数 | 组件每次渲染执行 | 组件每次渲染执行(但仅读缓存) |
// useGlobalUser.js(模块级全局状态Hook)
import { useState, useEffect, useCallback } from 'react';
// 模块作用域的变量:存储用户状态(闭包缓存)
let globalUserState = null;
// 存储所有订阅组件的更新函数(用于状态变更时通知组件重渲染)
let subscribers = new Set();
// 触发所有订阅组件重渲染
const notifySubscribers = () => {
subscribers.forEach(update => update());
};
// 自定义Hook:供组件调用
export function useGlobalUser() {
// 每个组件调用Hook时,创建一个本地的更新函数(触发组件重渲染)
const [, forceUpdate] = useState({});
// 组件挂载时订阅,卸载时取消订阅
useEffect(() => {
subscribers.add(forceUpdate);
return () => {
subscribers.delete(forceUpdate);
};
}, [forceUpdate]);
// 登录:更新全局状态并通知所有订阅组件
const login = useCallback((userInfo) => {
globalUserState = { ...userInfo };
notifySubscribers(); // 触发所有组件重渲染
}, []);
// 登出:重置全局状态
const logout = useCallback(() => {
globalUserState = null;
notifySubscribers();
}, []);
// 返回全局状态和操作方法
return {
user: globalUserState,
login,
logout,
isLogin: !!globalUserState
};
}
// Header组件
import { useGlobalUser } from './useGlobalUser';
export function Header() {
const { user, isLogin, logout } = useGlobalUser();
console.log('Header组件渲染:', user); // 验证是否共享状态
return (
<div>
<h1>网站头部</h1>
{isLogin ? (
<div>
欢迎 {user.name} <button onClick={logout}>登出</button>
</div>
) : (
<div>未登录</div>
)}
</div>
);
}
// Login组件
import { useGlobalUser } from './useGlobalUser';
export function Login() {
const { login } = useGlobalUser();
const handleLogin = () => {
// 模拟登录接口返回的用户信息
login({ id: 1, name: '张三', avatar: 'xxx.png' });
};
return (
<div>
<button onClick={handleLogin}>模拟登录</button>
</div>
);
}
// 主页组件
import { Header } from './Header';
import { Login } from './Login';
export function Home() {
return (
<div>
<Header />
<Login />
</div>
);
}
// useGlobalUser优化版.js
import { useState, useCallback, useEffect } from 'react';
// 模块作用域:存储全局状态的Hook实例(闭包缓存)
let globalHook = null;
// 全局状态的核心Hook(仅初始化一次)
function createGlobalUserHook() {
const [user, setUser] = useState(null);
const login = useCallback((userInfo) => {
setUser({ ...userInfo });
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
return {
user,
login,
logout,
isLogin: !!user,
// 暴露setUser供订阅逻辑使用(或直接用user作为依赖)
subscribe: (callback) => {
// 这里利用React的状态更新机制,无需手动维护订阅者
// 组件通过useEffect依赖user,自动触发重渲染
}
};
}
export function useGlobalUser() {
// 模块初始化时,创建全局Hook实例(仅执行一次)
if (!globalHook) {
globalHook = createGlobalUserHook();
}
// 返回全局状态和方法(所有组件共享同一个实例)
return globalHook;
}
// useGlobalState.js(通用全局状态Hook)
import { useState, useCallback, useEffect } from 'react';
// 模块作用域:存储所有全局状态(key-value形式)
const globalStates = new Map();
// 存储每个状态的订阅者
const subscribers = new Map();
// 创建或获取全局状态
function getOrCreateGlobalState(key, initialValue) {
// 如果状态已存在,直接返回
if (globalStates.has(key)) {
return globalStates.get(key);
}
// 新建状态
const [state, setState] = useState(initialValue);
// 批量更新(类似setState的函数式更新)
const setGlobalState = useCallback((updater) => {
setState(prev => {
const nextState = typeof updater === 'function' ? updater(prev) : updater;
// 通知该状态的所有订阅者
if (subscribers.has(key)) {
subscribers.get(key).forEach(update => update(nextState));
}
return nextState;
});
}, [key]);
const globalState = { state, setGlobalState };
globalStates.set(key, globalState);
subscribers.set(key, new Set());
return globalState;
}
// 通用全局Hook:支持传入key和初始值
export function useGlobalState(key, initialValue) {
const { state, setGlobalState } = getOrCreateGlobalState(key, initialValue);
// 组件本地更新函数(触发组件重渲染)
const [, forceUpdate] = useState(state);
// 订阅状态变更:状态更新时触发组件重渲染
useEffect(() => {
const subs = subscribers.get(key);
const update = (nextState) => forceUpdate(nextState);
subs.add(update);
// 初始触发一次渲染(同步初始状态)
forceUpdate(state);
return () => subs.delete(update);
}, [key, state]);
return [state, setGlobalState];
}
// 组件1:使用用户状态
const [user, setUser] = useGlobalState('user', null);
// 组件2:使用主题状态
const [theme, setTheme] = useGlobalState('theme', 'light');
// 组件3:更新全局状态
const handleChangeTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const handleLogin = () => {
setUser({ id: 1, name: '李四' });
};
// useGlobalUser.js
import { useState, useCallback } from 'react';
let globalHook = null;
console.log('模块初始化:执行一次'); // 仅打印一次
function createGlobalUserHook() {
console.log('创建全局状态:仅执行一次'); // 仅打印一次
const [user, setUser] = useState(null);
// ... 其余逻辑
}
export function useGlobalUser() {
console.log('调用useGlobalUser:组件渲染时执行'); // 组件每次渲染都会打印
if (!globalHook) {
globalHook = createGlobalUserHook();
}
return globalHook;
}
| 时机 | 日志输出 |
|---|---|
| 应用启动时 | 模块初始化:执行一次 |
| 第一个组件(Header)挂载 | 调用useGlobalUser:组件渲染时执行 → 创建全局状态:仅执行一次 |
| 第二个组件(Login)挂载 | 调用useGlobalUser:组件渲染时执行(无"创建全局状态"日志) |
| 组件重渲染(如登录后) | 两个组件都打印"调用useGlobalUser"(仍无"创建全局状态"日志) |
| 局限 | 说明 |
|---|---|
| 不支持服务端渲染(SSR) | 模块作用域状态在 SSR 中被所有请求共享,导致状态污染 |
| 状态更新缺乏可追踪性 | 没有 Redux DevTools 等工具支持,难以调试状态变更 |
| 不支持复杂状态逻辑 | 异步状态流、状态依赖、批量更新优化等需手动实现,成本较高 |
| 组件卸载后状态不自动清理 | 模块作用域状态一直存在直到应用刷新(可能内存泄漏) |
| 不支持状态切片隔离 | 多团队协作时容易出现状态 key 冲突(需手动规范命名) |