es-toolkit 的after函数:按调用次数控制执行时机的完整指南(compat 兼容版与原生版对比)
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
after是 es-toolkit 中一个典型的函数式工具:它包装一个函数,使其只有在被调用达到指定次数后才会真正执行。本文以 compat 兼容版文档 为主线,结合es-toolkit/compat与原生es-toolkit两套实现源码与测试用例,完整讲解其用法、边界行为、底层原理,以及何时该用哪个版本,帮助你用它在异步并发、初始化完成回调等场景中写出更清晰的控制流。
一、after是什么:一句话核心语义
after(n, func)创建一个受限函数(restricted function),前n - 1次调用不会触发func,从第n次调用起,func才会被执行,并且之后每次调用都会继续执行。
const restrictedFunction = after(n, func);它典型的应用场景包括:
- 多个异步操作完成后执行回调:例如同时发起多个请求,全部完成后才更新 UI;
- 初始化阶段结束后激活某个功能:在指定次数的事件(如按钮点击、拖拽、资源加载)发生后开启后续逻辑;
- 限流式的一次性开关逻辑:与
before、once等兄弟函数配合,构建调用次数驱动的状态机。
二、基本用法:从第 n 次调用开始执行
从es-toolkit/compat导入:
import { after } from 'es-toolkit/compat'; // 基础用法 const logAfterThree = after(3, () => { console.log('Executed from the 3rd call!'); }); logAfterThree(); // Not executed logAfterThree(); // Not executed logAfterThree(); // Logs "Executed from the 3rd call!" logAfterThree(); // Logs "Executed from the 3rd call!" (continues to execute)注意:第 3 次调用之后,函数会持续执行,而不是只执行一次。这与once的"只执行一次"语义不同。
组合多个异步任务完成回调
after最常见的实战用法是等待一批异步操作全部结束后再执行聚合回调:
import { after } from 'es-toolkit/compat'; const tasks = ['task1', 'task2', 'task3']; const allTasksComplete = after(tasks.length, () => { console.log('All tasks completed!'); }); // Called when each task completes tasks.forEach(task => { performAsyncTask(task, () => { console.log(`${task} complete`); allTasksComplete(); // Logs "All tasks completed!" on the 3rd call }); });这里n直接取tasks.length,无需硬编码任务数量,新增任务时逻辑自动保持正确。
传入 0 或负数:从第一次调用就立即执行
import { after } from 'es-toolkit/compat'; const immediate = after(0, () => console.log('Executed immediately')); immediate(); // "Executed immediately" const negative = after(-1, () => console.log('Executed immediately')); negative(); // "Executed immediately"这是compat 版特有的行为:当n <= 0时,第一次调用就会执行func。而原生版对负数会直接抛错(详见下文第五节),两者存在明显的行为差异,混用时务必注意。
参数与返回值
n(number):触发func执行前所需的调用次数。func(TFunc):被限制执行的函数。- 返回值 (
TFunc):一个新的受限函数,从第n次调用起执行原函数。
三、源码级剖析:compat 版是如何实现的
compat 版实现位于 src/compat/function/after.ts,完整逻辑如下:
import { toInteger } from '../util/toInteger.ts'; export function after<TFunc extends (...args: any[]) => any>(n: number, func: TFunc): TFunc { if (typeof func !== 'function') { throw new TypeError('Expected a function'); } n = toInteger(n); return function (this: any, ...args: Parameters<TFunc>) { if (--n < 1) { return func.apply(this, args); } } as TFunc; }可以拆解出三个关键设计:
- 入参校验:
func必须是一个函数,否则抛出TypeError('Expected a function')。测试 src/compat/function/after.spec.ts 中after(1, 42 as any)即验证了该错误路径。 - 整数化处理:
n会先经过toInteger(来自 src/compat/util/toInteger.ts)转换。这意味着NaN会被转为0,小数会被截断取整——这正是文档中"0 或负数立即执行"行为背后的机制。测试中testAfter(NaN, 1)结果为1,证实NaN被强制转为0后第一次调用即执行。 - 闭包计数器与 this 绑定:返回的受限函数是普通函数(非箭头函数),通过
--n < 1判断是否放行,并使用func.apply(this, args)保留调用方的this上下文。测试中object.after()能正确累加this.count(得到2),验证了 this 绑定行为。
compat 版为什么"慢"?
兼容版文档明确指出:由于涉及复杂的类型校验和整数转换处理(typeof func检查、toInteger转换),该版本运行速度较慢。因此文档给出的建议是:
优先使用 es-toolkit 原生、更快的 after。
这也解释了为什么 es-toolkit 提供了两个after——一个追求与 lodash 完全兼容的语义(compat),一个追求极致性能与更严格的类型约束(原生)。
四、原生版实现:更快、更严格
原生版位于 src/function/after.ts:
export function after<F extends (...args: any[]) => any>( n: number, func: F ): (...args: Parameters<F>) => ReturnType<F> | undefined { if (!Number.isInteger(n) || n < 0) { throw new Error(`n must be a non-negative integer.`); } let counter = 0; return (...args: Parameters<F>) => { if (++counter >= n) { return func(...args); } return undefined; }; }与 compat 版相比,原生版有三个显著差异:
| 对比维度 | 原生版es-toolkit/function | 兼容版es-toolkit/compat |
|---|---|---|
| 导入路径 | es-toolkit/function | es-toolkit/compat |
非法n的处理 | 非整数或负数直接throw new Error | 经toInteger自动转换,NaN→0,不抛错 |
非法func的处理 | 无显式校验(由调用时自然失败) | 立即抛TypeError('Expected a function') |
this绑定 | 返回箭头函数,不保留调用方this | 返回普通函数,通过apply保留this |
| 计数器实现 | 独立counter变量递增判断 | 直接对n递减判断 |
| 返回值类型 | (...args) => ReturnType \| undefined | 断言为TFunc |
原生版文档(docs/reference/function/after.md)与对应测试(src/function/after.spec.ts)进一步确认了这些边界:
- 负数与
NaN均会抛出[Error: n must be a non-negative integer.]; after(0)首次调用即执行;- 第
n次调用前返回值均为undefined,参数会原样透传给func。
选型建议:如果你在迁移 lodash 代码、依赖 lodash 的宽松语义(如NaN自动归零、负数立即执行、保留this),请使用 compat 版;如果你编写新代码、追求更高执行效率与严格的参数约束,请使用原生版。
五、与before的关系:一对相反的兄弟
compat 版源码注释将其描述为 "The opposite of_.before"。before限制函数最多执行 n - 1 次(第 n 次起不再执行),而after是第 n 次起才开始执行。二者组合可以精确表达"执行窗口":例如用before(5, cb)限制事件处理最多触发 4 次,或用after(3, cb)保证第 3 次之后才开始响应。它们的实现思路也互为镜像——before内部同样是--n > 0形式的计数器判断。
六、测试佐证:行为契约一目了然
两套测试用例共同构成了after的行为契约:
- 计数语义(src/compat/function/after.spec.ts):
testAfter(5, 4)为 0(未到第 5 次不执行)、testAfter(5, 5)为 1(第 5 次执行一次)、testAfter(0, 0)为 0(不立即执行)、testAfter(0, 1)为 1(调用一次即执行); - 严格校验(src/function/after.spec.ts):负数、
NaN抛错;n次调用前返回undefined;参数正确透传(afterFn(1, 2)返回3)。
这些测试文件可以直接作为行为参考,也可以在你的项目中以相同用例验证自定义的计数逻辑。
七、小结
after用一次闭包计数器,把"次数条件"从业务代码中抽离出来,让异步并发完成回调、初始化完成触发等场景的表达更加声明化。理解 compat 版(宽松、兼容 lodash、保留this)与原生版(严格、快速、无类型转换开销)的差异,你就能在迁移旧代码与编写新代码之间做出正确选择。若追求极致性能,请按官方建议优先使用原生版 after(es-toolkit/function),并将 compat 版保留给确实需要 lodash 语义的场景。
【免费下载链接】es-toolkitA modern JavaScript utility library that's 2-3 times faster and up to 97% smaller, a major upgrade to lodash.项目地址: https://gitcode.com/GitHub_Trending/es/es-toolkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考