直接让ai生成文章,水一下,其实市面上有很多的工具库有这个功能,但是我只想要这个,更易懂。

js

10:40 · 2026年06月10日 · 周三 show: --

前端竞态安全管道,之前一直是自己去加一个全局的版本号来实现,指挥ai封装了一个。

直接让ai生成文章,水一下,其实市面上有很多的工具库有这个功能,但是我只想要这个,更易懂。

competitionFn —— 前端竞态安全管道

一、先说问题:什么是"竞态"?

想象一个常见场景:用户在搜索框输入关键词,每次输入都会请求接口获取结果。

用户输入 "A" → 发起请求①(慢,需要 2 秒)
用户输入 "B" → 发起请求②(快,只要 0.5 秒)

请求②先回来了,页面显示了 B 的结果。但 1.5 秒后,请求①也回来了,页面被 A 的结果覆盖了。

用户搜的是 "B",看到的却是 "A" 的结果 —— 这就是竞态问题

类似场景还有很多:

  • 切换 Tab 时,上一个 Tab 的请求还没回来
  • 短时间内多次点击按钮
  • 下拉刷新时,旧数据的新请求覆盖了新数据

二、传统做法:手动管理版本号

let version = 0;

async function search(keyword: string) {
    const mine = ++version;
    const data = await fetchData(keyword);   // 异步请求
    if (mine !== version) return;             // 手动判断:如果不是最新的就放弃
    this.result = data;                       // 只有最新的才更新
}

痛点:

  • 每个地方都要自己声明 version 变量,容易忘
  • 如果步骤多了(请求 → 处理 → 再请求 → 再处理),每步之间都要判断
  • 如果一个步骤内部有多个异步操作,中间也想判断,写起来更麻烦

三、competitionFn:一个通用的竞态安全方案

核心思想

给每次调用一个版本号,任何时刻只有最新版本能继续执行,旧版本自动作废。

函数签名

competitionFn(key: string, steps: any[])
参数说明
key唯一标识。相同 key 的调用互斥,不同 key 互不影响
steps步骤数组,按顺序执行。每步可以是 Promise 或函数

ctx 上下文对象

每个 step 函数会收到 (上一步的结果, ctx),其中 ctx 提供:

属性/方法说明
ctx.mine当前调用的版本号(数字,创建时快照,不会变)
ctx.isLatest()检查当前调用是否仍然是最新的。可在任何异步操作后调用

四、怎么用?

1. 最简单的用法:直接传 Promise

async function search(keyword: string) {
    await competitionFn('user-search', [
        this.service.search(keyword),       // Promise → 自动等待
        (data) => {
            this.result = data;             // 只有最新调用才会走到这里
        },
    ]);
}

快速连搜三次(key 相同):

  • 第一次:请求慢 → 被第二次取代 → 结果丢弃
  • 第二次:请求慢 → 被第三次取代 → 结果丢弃
  • 第三次:请求完成 → 更新页面 ✅

2. 多步骤串联

await competitionFn('load-detail', [
    this.service.fetchList(),                            // 第1步:请求列表
    (list, ctx) => {                                     // 第2步:保存列表
        ctx.selectedId = list[0]?.id;
    },
    (list) => this.service.fetchDetail(list[0]?.id),     // 第3步:用第1步的结果请求详情
    (detail, ctx) => {                                   // 第4步:展示详情
        this.detail = detail;
    },
]);

每一步之间都会自动检查版本,如果已经不是最新调用,直接停止。

3. 在步骤内部手动检查(关键能力)

有时候一个步骤内部有多个异步操作,你想在每个 await 之后确认自己还没过时:

await competitionFn('save-form', [
    async (_prev, ctx) => {
        // 第一个异步操作
        const validateResult = await this.service.validate(formData);
        if (!ctx.isLatest()) return;    // 👈 等完后检查:是否已被新调用取代?

        // 第二个异步操作
        const saveResult = await this.service.save(formData);
        if (!ctx.isLatest()) return;    // 👈 再检查一次

        this.result = saveResult;
    },
]);

ctx.isLatest() 让你可以在步骤内部的任意位置检查,不再局限于"步骤之间"。

4. 不同 key 互不干扰

// 这两个调用用的是不同的 key,互不影响
await competitionFn('search-api', [...]);    // key = 'search-api'
await competitionFn('suggest-api', [...]);   // key = 'suggest-api'

五、一张图总结

用户操作          调用① (key=搜索)              调用② (key=搜索)
  │                   │                            │
  ├─ 输入A ──→  mine=1, 开始请求A ──→              │
  │                   │                            │
  ├─ 输入B ────────────────────────────→  mine=2, 开始请求B
  │                   │                            │
  │             A的结果回来了                B的结果回来了
  │                   │                            │
  │            isLatest()?                 isLatest()?
  │            1 ≠ 2 → false ❌             2 == 2 → true ✅
  │            自动丢弃                      更新页面

六、与传统写法的对比

手动管理版本号competitionFn
每个场景都要声明变量✅ 是❌ 不需要,传 key 即可
多步骤间自动检查❌ 每步手写✅ 自动
步骤内部也能检查❌ 要自己暴露方法ctx.isLatest()
上下文共享(ctx)❌ 自己管理✅ 自动传递

七、实现原理

完整源码不到 20 行,核心逻辑非常简单:

// 用一个对象记录每个 key 当前的最新版本号
const _competitionVersions: Record<string, number> = {};

export async function competitionFn(key: string, steps: any[]) {
    // 1. 版本号 +1,代表"我是一次新的调用"
    if (!_competitionVersions[key]) _competitionVersions[key] = 0;
    const mine = ++_competitionVersions[key];

    let prevResult: any;

    // 2. 构造上下文对象,暴露给使用者
    const ctx: any = {
        /** 当前调用的版本号(快照,不会变) */
        mine,
        /** 实时检查:我的版本号还等于最新版本号吗? */
        isLatest: () => mine === _competitionVersions[key],
    };

    // 3. 逐步执行
    for (const step of steps) {
        // 每步之前先检查:如果已经不是最新调用,直接退出
        if (!ctx.isLatest()) return;

        if (typeof step === 'function') {
            // 函数 → 执行,传入 (上一步结果, ctx)
            prevResult = await step(prevResult, ctx);
        } else {
            // Promise → 直接等待
            prevResult = await step;
        }
    }

    return prevResult;
}

三句话看懂:

  1. 每次调用把 key 对应的版本号 +1,记为自己的 mine
  2. 每执行一步之前,比较 mine 是否还等于最新版本号,不是就退出
  3. ctx.isLatest() 是同一个比较逻辑的封装,供步骤内部随时调用

八、注意事项

  1. key 要唯一:同一个业务场景用同一个 key,不同业务用不同的 key
  2. 旧调用不会 cancel 网络请求:它只是让旧调用"不再继续执行后续步骤",请求本身仍在进行
  3. ctx 可以存临时数据ctx.xxx = ... 在不同 step 之间共享,但只属于当前调用
最后一次更新时间: 10:40 · 2026年06月10日 · 周三