必须缓存promise而非最终值,因为异步资源加载耗时、可能失败且需共享生命周期;缓存promise可确保同一参数下所有调用复用同一异步任务,实现真正的结果单例化。

要实现具备“结果单例化”特征的异步资源加载单例器,核心不是缓存实例对象本身,而是缓存**异步操作的结果(Promise)**,确保同一请求参数下,所有调用都复用同一个 Promise,避免重复发起网络请求、重复解析或重复初始化。闭包天然适合封装这个“已发起但未完成”的 Promise 状态,且能严格隔离外部干扰。
为什么必须缓存 Promise 而非最终值
异步资源(如远程配置、i18n 语言包、字体文件、权限元数据)往往具有以下特点:
- 加载耗时,重复触发会浪费带宽与渲染等待时间
- 结果可能随环境变化(如 locale、theme),但同一参数组合下结果应恒定
- 加载过程可能失败,需统一错误处理,而非每个调用都单独 catch
- 多个组件/模块可能同时请求相同资源,它们应共享同一加载生命周期
若只缓存最终值(如 config 对象),则首次失败后无法重试;若每次调用都 new Promise,则失去“单例化”意义。因此,缓存的是 Promise 实例本身——它代表一次确定的异步任务执行过程。
基础结构:闭包内缓存 Promise Map
使用一个私有 Map 存储「参数标识 → 正在进行的 Promise」,保证相同 key 只有一个 Promise 在运行:
const ConfigLoader = (function () {<br> const cache = new Map(); // key: url 字符串,value: Promise<any><br><br> function load(url) {<br> if (cache.has(url)) {<br> return cache.get(url);<br> }<br><br> const promise = fetch(url)<br> .then(res => {<br> if (!res.ok) throw new Error(`HTTP ${res.status}`);<br> return res.json();<br> })<br> .catch(err => {<br> cache.delete(url); // 失败后清除,允许下次重试<br> throw err;<br> });<br><br> cache.set(url, promise);<br> return promise;<br> }<br><br> return { load };<br>})();</any>
调用 ConfigLoader.load('/conf.json') 多次,返回的始终是同一个 Promise,无论是否已 resolve/reject。
支持复合参数与键生成
真实场景中,加载常依赖多个参数(如 locale + version + env)。不能简单用 JSON.stringify,需稳定、可比较的 key:
- 对参数对象做浅层排序后序列化(推荐使用
JSON.stringify(Object.keys(obj).sort().reduce((a, k) => {...}, {}))) - 或定义显式 key 生成函数,例如:
const getKey = ({ locale, version }) => `${locale}-${version}` - 避免函数、Symbol、undefined 等不可序列化值参与 key 构建
key 一旦生成错误,会导致“假多例”——本该复用的请求被当作新请求发起。
增强能力:自动清理与强制刷新
生产环境中需可控性:
- 提供
clear(key)或clearAll()方法,用于版本切换、登录态变更后主动失效缓存 - 暴露
load({ url, force: true })参数,绕过缓存重新发起(内部先cache.delete(key)) - 不建议在 Promise resolve 后自动从 cache 删除——否则后续调用会再次发起请求,破坏“结果单例”语义;应保留至显式清理或页面卸载
注意:不要在 Promise.then 中修改 cache 内容,那会破坏引用一致性;清理动作必须发生在 Promise 创建前。










