功能说明
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;
};
}