← 中文资源

简体中文版本

只执行一次的函数包装

包装同步与异步函数,使初始化逻辑最多执行一次,并共享缓存结果或进行中的 Promise。

阅读英文原文

功能说明

once 包装同步函数,首次调用时执行并缓存返回值,后续调用直接返回同一结果。onceAsync 针对异步函数缓存首次创建的 Promise,从而让并发调用共享同一次进行中的请求,避免重复加载配置、重复连接数据库或重复发送初始化事件。两者都保留调用时的 this 与参数,适合懒初始化、单例加载和幂等注册。

关键代码

同步版本用 called 标志在调用前置位,确保即使原函数抛错也不会重试,result 保存首次返回值。异步版本用 Promise.resolve().then 把执行推迟到微任务,既统一了同步抛错与 Promise 拒绝的行为,也避免在赋值前同步执行导致重入问题。由于缓存的是 Promise,所有调用方观察到相同的成功值或同一个错误。

使用注意

同步版本在首次执行抛错后仍标记为已调用,后续返回 undefined;若希望失败后允许重试,应使用带错误重置的变体。异步版本的 Promise 若永久 pending,所有调用方都会一起等待,初始化逻辑应设置超时。参数只在首次调用时生效,后续调用的不同参数会被忽略,因此不适合参数敏感的查询。两次包装同一函数会产生两个独立缓存。

验证方式

测试同步函数连续调用三次只执行一次,返回值严格相等,并验证 this 与参数传递给首次调用。测试首次抛错后不会再次执行。异步版本用计数器确认并发三次只调用一次,且三个 Promise 结果一致;再测试拒绝后不会自动重试。最后覆盖非函数输入、参数不同的后续调用以及微任务时序。

代码示例

export function once(fn) {
  if (typeof fn !== "function") throw new TypeError("fn must be a function");
  let called = false;
  let result;
  return function onceWrapper(...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

export function onceAsync(fn) {
  if (typeof fn !== "function") throw new TypeError("fn must be a function");
  let promise;
  return function onceAsyncWrapper(...args) {
    if (!promise) {
      promise = Promise.resolve().then(() => fn.apply(this, args));
    }
    return promise;
  };
}