JavaScript多函数await异步实例时如何避免重复执行load方法
JavaScript异步单例加载优雅实现方案
问题核心原因
原有逻辑重复触发异步加载的根源是:第一次调用load()时,代码先执行await等待Promise完成,才会把结果赋值给instance变量。在Promise处于pending状态的窗口期(比如例子中t1到t6之间),后续调用load()判断时instance还是undefined,就会重复创建新的Promise任务,导致多次执行异步逻辑。
缓存Promise本身的写法已经可以解决重复执行的问题,只是每个业务方法都要手动写const instance = await this.load()存在重复代码,可以通过以下方案优化。
基础修复写法(零额外依赖)
如果不需要做架构层面的封装,只需要把原来的load方法调整赋值顺序即可解决重复执行问题:
let instancePromise; async load() { // 只要Promise已经创建过,不管是pending还是resolved状态都直接返回 if (instancePromise) return instancePromise; // 先把Promise实例存下来,再等待结果,避免窗口期重复创建 instancePromise = new Promise(resolve => setTimeout(() => { resolve([]); }, 5000)); return instancePromise; }
调整后执行逻辑完全符合预期:
- t1调用
load()创建Promise,t6时resolve - t3调用
load()直接返回已存在的pending状态Promise,不会创建新的定时器 foo和bar都会在t6同时拿到加载完成的实例,不会等到t8- t6之后调用
load()会直接返回已resolve的Promise,无额外异步开销
类场景优雅封装方案
针对类业务场景,可以通过以下方式减少重复代码:
方案1:统一实例访问入口
用私有字段缓存加载状态,通过getter统一暴露实例访问入口,不需要单独维护load方法的调用:
export default class { // 私有字段缓存加载Promise和最终实例,避免外部篡改 #modulePromise = null; #instance = null; constructor() { // 实例化时自动触发预加载,注释掉即可实现懒加载(第一次访问实例时才加载) this.#loadModule(); } #loadModule() { if (!this.#modulePromise) { this.#modulePromise = import('amodule') .then(({ default: Ctor }) => new Ctor()) .then(instance => { this.#instance = instance; return instance; }); } return this.#modulePromise; } // 统一访问入口,自动处理加载等待 get instance() { // 实例加载完成后直接返回同步值,无异步开销 if (this.#instance) return this.#instance; return this.#loadModule(); } async foo() { const instance = await this.instance; // 直接使用instance写业务逻辑 } async bar({ params }) { const instance = await this.instance; // 直接使用instance写业务逻辑 } }
方案2:装饰器自动注入实例
如果项目支持装饰器语法(TS或配置了Babel装饰器插件),可以把实例等待逻辑抽成通用装饰器,业务方法完全不需要写加载相关代码:
// 通用实例等待装饰器 function InjectInstance(target, context) { return async function(...args) { const instance = await this.instance; return target.call(this, instance, ...args); } } export default class { #modulePromise = null; #instance = null; #loadModule() { if (!this.#modulePromise) { this.#modulePromise = import('amodule') .then(({ default: Ctor }) => new Ctor()) .then(instance => { this.#instance = instance; return instance; }); } return this.#modulePromise; } get instance() { if (this.#instance) return this.#instance; return this.#loadModule(); } @InjectInstance foo(instance) { // 直接使用注入的instance,不需要写任何加载逻辑 } @InjectInstance bar(instance, { params }) { // 第一个参数为自动注入的实例,后续参数为业务入参 } }
方案3:Proxy自动代理(适配多方法场景)
如果类中需要使用实例的方法很多,不想逐个加装饰器,可以通过Proxy自动代理所有方法调用,统一注入实例:
export default class { #modulePromise = null; #instance = null; constructor() { this.#loadModule(); // 返回代理对象,自动拦截方法调用 return new Proxy(this, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); // 非方法属性、私有方法不做代理 if (typeof value !== 'function' || prop.toString().startsWith('#')) { return value; } // 业务方法自动等待实例加载完成后注入 return async function(...args) { const instance = await target.instance; return value.call(target, instance, ...args); } } }) } #loadModule() { if (!this.#modulePromise) { this.#modulePromise = import('amodule') .then(({ default: Ctor }) => new Ctor()) .then(instance => { this.#instance = instance; return instance; }); } return this.#modulePromise; } get instance() { if (this.#instance) return this.#instance; return this.#loadModule(); } async foo(instance) { // 直接使用instance } async bar(instance, { params }) { // 直接使用instance } }
所有方案的核心逻辑一致:缓存pending状态的Promise对象,而不是等Promise resolve完成后再缓存结果。只要第一次触发加载时就把Promise存下来,后续所有调用都共享同一个Promise,自然不会出现重复执行的问题。
内容的提问来源于stack exchange,提问作者gremo
相关产品推荐
相关产品推荐

