前言
场景:我们在日常项目中,是否遇到过,一个页面内,不同层次的组件(如:父子组件)内都调用了相同接口,导致进入页面,接口就重复多次调用,而你的理想状态是:我进入页面,管你接口在这个页面的什么地方调用,管你这个接口调用了N次,你给我只执行请求一次,有结果,所有相同请求的地方都用这个结果,如果请求失败,那么大家一起失败
我们依然延续之前的文章风格, 场景=>
方案=>
示例 的模式加注释
方案
我们需要一个
异步同源
对象来支撑我们的场景,具体作用就是进行请求任务的拦截和结果处理
js
// 异步同源对象
const asyncSameSource = {
map: new Map(), // 任务对象
waitMap: new Map(), // 等待map对象
// 缓存中是否有该任务
has (name) {
return this.map.has(name)
},
// 从缓存中获取该任务的结果
get (name) {
return this.map.get(name)
},
// 缓存该任务的结果
set (name, value) {
this.map.set(name, value)
},
// 删除该任务的结果
delete (name) {
this.map.delete(name)
},
// 添加等待该任务结果的函数
wait (name, resolve, reject) {
const waits = this.waitMap.get(name) || []
waits.push({ resolve, reject })
this.waitMap.set(name, waits)
},
// 派发等待该任务结果的函数
dispatchWait (name, val, isError = false) {
if (this.waitMap.has(name)) {
const waits = this.waitMap.get(name)
for (let i = 0; i < waits.length; i++) {
const wait = waits[i]
isError ? wait.reject(val) : wait.resolve(val)
}
}
},
// 获取结果函数
getResult (
name, // 任务名字,字符串类型
requestApi, // 请求接口,返回是一个promise
cache = false // 是否缓存结果
) {
// 任务名字
const TaskName = `${name}TaskName`
// 出错任务名字
const TaskErrName = `${name}TaskErrName`
// 如果缓存中没有这个任务,同时这个任务也没有出错
if (!this.has(name) && !this.has(TaskName)) {
console.log('%c [ 第一个任务 ]-1354', 'font-size:13px; background:pink; color:#bf2c9f;', )
// 那么对该次任务进行状态标记,关闭 if 入口,让后面相同任务进入 else 入口
this.set(TaskName, true)
// 开始请求数据
return requestApi()
.then(result => { // 成功请求数据后
// 将成功返回的数据结果设置到缓存中
this.set(name, result)
// 触发等待任务列表,进行通知,当然是成功的通知,一对都对
this.dispatchWait(name, result)
// 返回结果
return result
})
.catch(err => { // 失败
// 缓存错误任务
this.set(TaskErrName, err)
// 触发等待任务列表,进行通知, 当然是失败的通知,一错都错
this.dispatchWait(name, err, true)
// 抛出错误
throw err
}).finally(() => {
// 成功失败与否,都已经出了结果,就把缓存标记状态删除,任务结束
this.delete(TaskName)
// 如果不缓存结果, 缓存1秒后删除 任务 和 错误任务
if (!cache) {
// 如果不缓存结果,在1000毫秒内再次发起的相同任务依然使用缓存结果
setTimeout(() => {
this.delete(name)
this.delete(TaskErrName)
}, 1000)
}
})
} else {
// 如果同个任务已经在在缓存中了,就走到这里
return new Promise((resolve, reject) => {
// 先判断缓存中是否有任务结果
if (this.has(name)) {
// 有就直接返回结果
resolve(this.get(name))
} else if (this.has(TaskErrName)) { // 再判断缓存中是否有该次任务的错误任务
// 有就直接返回错误
reject(this.get(TaskErrName))
} else {
// 没有就缓存等待任务,等待任务列表中,有结果了,就通知等待任务列表
this.wait(name, resolve, reject)
console.log('%c [ 等待任务 ]-1398', 'font-size:13px; background:pink; color:#bf2c9f;', this.waitMap)
}
})
}
},
// 清空任务列表
clear () {
this.map.clear()
}
}
我们 发现 和 上篇 文章 前端 同时多次调用同个接口,如何只触发一次而不是触发多次 有异曲同工之妙,都是用 promise 为基础 用resolve 进行操作,下面我们来看下示例
示例
问题示例
js
// api请求 模拟
const api = {
getUserInfo(){
console.log('调用 getUserInfo 接口')
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve('用户信息')
},1000)
})
},
getProductInfo(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject('错误')
},300)
})
}
}
// 页面 3个 地方 调用了相同接口 3次
api.getUserInfo().then(res=>{
console.log('第一个相同请求', res)
})
api.getUserInfo().then(res=>{
console.log('第二个相同请求', res)
})
api.getUserInfo().then(res=>{
console.log('第三个相同请求', res)
})
结果如图所示

我们模拟api 请求,在页面 执行了 3次相同接口,事实上这个接口也确实执行了 3次,如何优化,看我们下面进一步操作
优化示例
js
// api请求 模拟
const api = {
getUserInfo(...args){
const requestUserInfoApi = (..._args)=> {
console.log('%c [ 调用 getUserInfo 接口 ]-1235', 'font-size:13px; background:pink; color:#bf2c9f;', _args)
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve('用户信息')
},1000)
})
}
return asyncSameSource.getResult('getUserInfo', ()=>requestUserInfoApi(...args),true)
},
getProductInfo(...args){
const requestProductInfoApi = (..._args)=> {
console.log('%c [ 调用 getProductInfo 接口 ]-1235', 'font-size:13px; background:pink; color:#bf2c9f;', _args)
return new Promise((resolve, reject)=>{
setTimeout(()=>{
reject('商品信息')
},1000)
})
}
return asyncSameSource.getResult('getProductInfo', ()=>requestProductInfoApi(...args),true)
}
}
// 页面 3个 地方 调用了相同接口 3次
api.getUserInfo(1).then(res=>{
console.log('第一个相同请求', res)
})
api.getUserInfo(2).then(res=>{
console.log('第二个相同请求', res)
})
api.getUserInfo(3).then(res=>{
console.log('第三个相同请求', res)
})
打印结果如下

可以看到
getUserInfo
接口值调用了一次,3个相同请求也都拿到了结果,符合我们的期望,具体项目中,在 我们封装请求的源头 get、post 进行包装一层是最好的,下面我只是象征性的举个样板例子
实际项目中,我们从源头请求进行处理,示例如下
某某页面
js
// 象征性的 标识基于 axios 封装过的 请求http
import http from './lib/axios'
/**
* post方法
* @param {String} url 请求的url地址
* @param {Object} params 请求时携带的参数
* @param {Object} config 额外请求配置参数,便于拦截器里使用
*/
export function post (url, params = {}, config = {}) {
return asyncSameSource.getResult(url + JSON.stringify(params), () => http
.post(url, params, config)
.then(res => {
if (res && res.data) {
return res.data
} else {
throw new Error(res)
}
}))
}
这样就不用在 不用的api接口里面去处理,直接源头处理更为直接
结语
本篇文章是对 前端 同时多次调用同个接口,如何只触发一次而不是触发多次 文章的
会敲代码的柯基
给出的场景而写, 我们项目中出现该场景的情况还是比较多的,希望对大家有用,如有疑问,欢迎留言,有更好的 idea,欢迎一起讨论,感谢,欢迎提出各种场景,一起探讨