学习笔记:实现高并发场景下的请求合并函数 syncOnce
一、 核心问题与目标
在前端开发中,可能会遇到多个组件在同一时间(例如页面加载时)请求同一个接口获取相同数据的场景。这会导致发出多个重复的网络请求,造成资源浪费。
目标: 创建一个高阶函数 syncOnce,它接受一个异步函数作为参数,并返回一个新函数。这个新函数具备以下特性:
- 当被并发调用(即第一次调用尚未完成时,又发起了后续调用)时,只执行一次底层的异步函数。
- 所有并发的调用都会收到这唯一一次执行的结果。
javascript
// 目标函数签名
function syncOnce(asyncFunction) {
// ... 实现 ...
return function(...args) {
// ... 返回一个新的、经过优化的函数 ...
}
}
// 使用场景
const loadData = async (id) => { /* ...发请求... */ };
const getOnce = syncOnce(loadData);
// 多个组件同时调用
getOnce(1); // 发起网络请求
getOnce(1); // 不会发起新请求,等待上一个结果
getOnce(1); // 同上二、 初版实现:处理无参数的情况
最简单的思路是利用一个状态标记(flag)来判断当前是否有请求正在进行中。
1. 实现思路
- 闭包变量:使用闭包来维护两个状态变量:
isPending(boolean): 标记是否有请求正在处理。cbs(callbacks, array): 一个回调函数数组,用于存储在请求进行中时发起的其他调用的resolve和reject函数。
- 执行逻辑:
- 当包装后的函数被调用时,立即将其
resolve和reject回调存入cbs数组。 - 检查
isPending状态:- 如果为
true,说明已有请求在途,直接return,等待被处理。 - 如果为
false,说明是首次调用,将isPending设为true,然后执行原始的异步函数。
- 如果为
- 异步函数执行完毕后(无论成功或失败):
- 遍历
cbs数组,用得到的结果或错误,统一处理所有等待中的 Promise。
- 遍历
- 在
finally块中,重置isPending为false并清空cbs数组,以便函数可以被再次调用。
- 当包装后的函数被调用时,立即将其
2. 代码实现
javascript
function syncOnce(promiseFn) {
let isPending = false;
const cbs = []; // 存储 resolve 和 reject
return function(...args) {
return new Promise((resolve, reject) => {
// 无论如何,先把当前调用的 resolve/reject 入队
cbs.push({ resolve, reject });
// 如果正在请求中,则直接返回,等待被唤醒
if (isPending) {
return;
}
isPending = true;
promiseFn(...args)
.then(data => {
// 成功后,唤醒所有等待的 promise
cbs.forEach(({ resolve }) => resolve(data));
})
.catch(err => {
// 失败后,同样处理
cbs.forEach(({ reject }) => reject(err));
})
.finally(() => {
// 重置状态,以便下次可以重新发起请求
isPending = false;
cbs.length = 0;
});
});
};
}💡 优化点:
将
cbs.push操作无条件地放在检查isPending之前,可以简化逻辑。
三、 问题升级与最终实现:支持不同参数
初版实现有一个明显缺陷:它无法区分带有不同参数的调用。例如,getOnce(1) 和 getOnce(2) 会被错误地认为是同一个请求,导致 getOnce(2) 拿到 getOnce(1) 的结果。
1. 解决方案:使用 Map
为了管理不同参数的请求状态,我们需要一个更强大的数据结构来取代单一的 isPending 标记。Map 是理想的选择。
- Key: 将函数的参数序列化(例如
JSON.stringify)作为Map的唯一键。 - Value: 存储与该
key对应的独立状态,包括isPending和cbs数组。
2. 最终实现思路
- 创建一个全局的
Map来存储所有请求的状态。 - 当包装函数被调用时,将传入的参数
args序列化成一个key。 - 检查
Map中是否存在该key:- 如果不存在,为这个
key初始化一个状态对象{ isPending: false, cbs: [] }并存入Map。
- 如果不存在,为这个
- 从
Map中获取当前key对应的状态对象state。 - 后续逻辑与初版实现类似,但所有操作都针对
state对象(state.isPending,state.cbs)。 - 关键一步:清理。在异步操作的
finally块中,必须从Map中删除当前的key(map.delete(key))。这确保了合并仅针对并发调用。当一组并发请求完成后,下一次使用相同参数的调用将能发起新的请求。
3. 最终代码 (通用版本)
javascript
/**
* 接受一个异步函数,返回一个新函数。
* 当新函数被并发调用时,只有第一次会真正执行,
* 后续调用会返回第一次执行的 Promise。
* 不同参数的调用会被视为不同的请求。
* @param {function} promiseFn 一个返回 Promise 的异步函数
*/
function syncOnce(promiseFn) {
// 使用 Map 来存储不同参数对应的请求状态
const cache = new Map();
return function(...args) {
// 1. 将参数序列化,作为唯一键
// 注意:这里简单 stringify,复杂对象可能需要更稳健的序列化方案
const key = JSON.stringify(args);
return new Promise((resolve, reject) => {
// 2. 检查缓存中是否有此 key 的状态
let state = cache.get(key);
if (!state) {
// 如果没有,初始化状态
state = {
isPending: false,
cbs: []
};
cache.set(key, state);
}
// 3. 将当前调用的回调入队
state.cbs.push({ resolve, reject });
// 4. 如果此 key 对应的请求已在进行中,则直接返回
if (state.isPending) {
return;
}
// 5. 如果是新请求,则执行
state.isPending = true;
promiseFn(...args)
.then(data => {
// 成功,用结果 resolve 所有等待的 promise
state.cbs.forEach(({ resolve }) => resolve(data));
})
.catch(err => {
// 失败,用错误 reject 所有等待的 promise
state.cbs.forEach(({ reject }) => reject(err));
})
.finally(() => {
// 6. 请求完成,从缓存中删除该 key,以便下次能重新请求
cache.delete(key);
});
});
};
}
// --- 示例 ---
const getUser = (id) => {
console.log(`发起请求: 获取用户 ${id}...`);
return new Promise(resolve => setTimeout(() => resolve({ id, name: `用户${id}` }), 1000));
};
const getOnceUser = syncOnce(getUser);
console.log("并发调用 getOnceUser(1)...");
getOnceUser(1).then(console.log);
getOnceUser(1).then(console.log);
console.log("并发调用 getOnceUser(2)...");
getOnceUser(2).then(console.log);
// 预期输出:
// 并发调用 getOnceUser(1)...
// 发起请求: 获取用户 1...
// 并发调用 getOnceUser(2)...
// 发起请求: 获取用户 2...
// (1秒后)
// { id: 1, name: '用户1' }
// { id: 1, name: '用户1' }
// { id: 2, name: '用户2' }四、 总结
syncOnce 函数是前端性能优化中的一个实用技巧,其核心思想可以总结为:
- 闭包:用于创建私有作用域,存储
Map缓存。 - 状态标记
isPending:防止对同一个资源发起重复请求。 Map数据结构:通过将参数序列化为键,实现了对不同参数调用的精细化管理。- 请求后清理
map.delete(key):这是设计的关键,它将函数的适用范围限定在“处理并发请求”,而不是实现一个永久性的数据缓存。