Skip to content

Instantly share code, notes, and snippets.

@PrintNow
Last active December 14, 2025 14:52
Show Gist options
  • Select an option

  • Save PrintNow/6edd6926b36a618a26aca49aba764a43 to your computer and use it in GitHub Desktop.

Select an option

Save PrintNow/6edd6926b36a618a26aca49aba764a43 to your computer and use it in GitHub Desktop.
一个极简的批量请求封装(DataLoader 风格) 特点: - 不依赖任何第三方库 - 自动合并同一事件循环内的请求 - 自动去重(同一个 key 只请求一次) - 对调用方保持 Promise 语义 适用场景: - React 多组件并发请求 - 减少频繁的小请求 - 测试环境 / mock 环境
// 具体使用示例看评论区:https://gist.github.com/PrintNow/6edd6926b36a618a26aca49aba764a43?permalink_comment_id=5904725#gistcomment-5904725
/**
* createBatchFetcher
*
* 一个极简的批量请求封装(DataLoader 风格)
*
* 特点:
* - 不依赖任何第三方库
* - 自动合并同一事件循环内的请求
* - 自动去重(同一个 key 只请求一次)
* - 对调用方保持 Promise 语义
*
* 适用场景:
* - React 多组件并发请求
* - 减少频繁的小请求
* - 测试环境 / mock 环境
*/
/**
* 创建一个批量请求函数
*
* @param {Function} batchFetch
* 一个批量请求函数,接收 key 数组,返回 Promise
* Promise resolve 的结果必须是:
* { [key]: value }
*
* @returns {Function}
* fetchByKey(key) => Promise<value>
*/
export function createBatchFetcher(batchFetch) {
if (typeof batchFetch !== 'function') {
throw new Error('createBatchFetcher 必须传入 batchFetch 函数');
}
// ==========================
// 内部状态(每个 fetcher 独立)
// ==========================
const pendingMap = new Map();
/**
* key => {
* promise: Promise,
* resolve: Function,
* reject: Function
* }
*/
// 是否已经安排了批量请求
let scheduled = false;
// ==========================
// 调度批量请求(microtask)
// ==========================
function scheduleFlush() {
// 已经安排过批量请求,直接返回
if (scheduled) return;
scheduled = true;
// 使用 microtask,保证同一 tick 内的调用被合并
Promise.resolve().then(async () => {
scheduled = false;
if (pendingMap.size === 0) return;
// 快照当前批次
const batch = new Map(pendingMap);
pendingMap.clear();
const keys = Array.from(batch.keys());
try {
const result = await batchFetch(keys);
/**
* 期望 batchFetch 返回的数据结构:
* {
* [key]: value
* }
*/
keys.forEach(key => {
const handler = batch.get(key);
if (!handler) return;
if (result && key in result) {
handler.resolve(result[key]);
} else {
handler.reject(
new Error(`批量结果中缺少 key: ${String(key)}`)
);
}
});
} catch (err) {
// 批量请求整体失败,所有 Promise 一起 reject
batch.forEach(({ reject }) => reject(err));
}
});
}
// ==========================
// 对外暴露的单条请求函数
// ==========================
return function fetchByKey(key) {
// 去重:同一个 key 在一个批次中只创建一个 Promise
if (pendingMap.has(key)) {
return pendingMap.get(key).promise;
}
let resolve;
let reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
pendingMap.set(key, { promise, resolve, reject });
// 安排批量请求
scheduleFlush();
return promise;
};
}
@PrintNow
Copy link
Author

PrintNow commented Dec 14, 2025

使用示例 1:真实网络请求(fetch)

import { createBatchFetcher } from './createBatchFetcher';
// 创建一个订单批量请求函数
const fetchOrder = createBatchFetcher(async (orderIds) => {
  const res = await fetch('/apis/orders/batch', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      ids: orderIds
    })
  });
  if (!res.ok) {
    throw new Error(`请求失败,状态码:${res.status}`);
  }
  // 假设返回结构:
  // { "123": {...}, "124": {...} }
  return await res.json();
});
// ==========================
// 使用方式
// ==========================
fetchOrder(123).then(order => {
  console.log('订单 123', order);
});
fetchOrder(124).then(order => {
  console.log('订单 124', order);
});
fetchOrder(125).then(order => {
  console.log('订单 125', order);
});
// 以上三个调用会被合并成一次请求

使用示例 2:测试 / Mock(setTimeout)

适合在 单元测试 / 本地调试 / 无后端环境 使用。

import { createBatchFetcher } from './createBatchFetcher';
// 使用 setTimeout 模拟网络请求
const mockFetchUser = createBatchFetcher((userIds) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      const result = {};
      userIds.forEach(id => {
        result[id] = {
          id,
          name: `User ${id}`,
          mock: true
        };
      });
      resolve(result);
    }, 300); // 模拟 300ms 网络延迟
  });
});
// ==========================
// 使用方式
// ==========================
mockFetchUser(1).then(user => {
  console.log('用户 1', user);
});
mockFetchUser(2).then(user => {
  console.log('用户 2', user);
});
mockFetchUser(3).then(user => {
  console.log('用户 3', user);
});
// 三个调用会被合并成一次 setTimeout 执行

设计说明

  • 批量合并基于 microtask(Promise.then)
  • 同一事件循环内的调用会自动合并
  • 每个 key 对应一个 Promise
  • 无缓存、无 TTL、无副作用
  • 每个 createBatchFetcher 实例是相互独立的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment