相似请求合并

前言

在日常开发中,有一种很常见的接口场景:例如,需要获取 typea 的数据,这时候的接口入参为 { type: ["a"] }。如果需要同时获取 typeab 的数据,则入参为 { type: ["a", "b"] }。在需要获取多种 type 数据的场景中,显而易见,只在一次请求里调用是最好不过的了。不过,如果后续需要将不同 type 的数据用作不同用途时,也就意味着,在请求成功后,需要再对数据进行拆分。

在同一个组件下,在请求后拆分,是很方便的。如果是非同一个组件下呢?设想另一种场景,组件 A 需要获取 { type: ["a"] } 的数据,组件 B 需要获取 { type: ["b"] } 的数据,那么可以在两者的父组件中统一调用接口 { type: ["a", "b"] } ,然后拆分数据后,分别将数据传给对应需要的组件,这种应该是很普遍的解决方案了。

再设想几个场景,如果组件 A 与 组件 B 不是兄弟组件呢?如果组件 A 需要在另一个组件中复用呢?这种通过在祖先组件内调用接口,获取数据后再传递给对应后代组件的方式,是不是就不太合适了。所以还是尽可能地将某个组件的内部逻辑与其他组件进行解耦,减少它们之间的关联,这样才能方便后续的复用。

这时候,你可能在想,那每个组件各自调用接口不就好了。但是呢,明明是同一个接口,只是入参中的某个参数不一样,却调用了多次,而且明明是可以同时传入多种类型参数的,嘶,总感觉不是很优雅。如果能把这些调用的接口合并成一个接口,获取数据后按照类型进行拆分,然后根据每个调用所需的类型返回对应类型的数据,那该多好呀。那么这能实现吗?当然可以!

收集类型

首先,先思考一个问题,如果一个函数同步调用了多次,那么如何只执行一次呢?比如,对于下面这段代码:

js 复制代码
function print() {
  console.log("print")
}

for (let i = 0; i < 10; i++) {
  print()
}

毫无疑问会打印 10print。如果只想打印 1print,该如何实现?

不妨先了解一个小知识点:Vue 中响应式数据更新的优化策略,它的主体思想就是:把需要触发的回调函数放进一个任务队列中,同时过滤掉相同的回调,并在下一次事件循环中执行队列中的所有回调。(下面的代码源自:《Vue.js 设计与实现》P63)

js 复制代码
// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,则什么都不做
  if (isFlushing) return
  // 设置为 true,代表正在刷新
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    // 结束后重置 isFlushing
    isFlushing = false
  })
}

按照这个思路把 print 改造一下,如下:

js 复制代码
function transform(callback) {
  return function () {
    // 每次调用时,将回调函数添加到 jobQueue 队列中
    jobQueue.add(callback)
    // 调用 flushJob 刷新队列
    flushJob()
  }
}

const print = transform(() => {
  console.log("print")
})

for (let i = 0; i < 10; i++) {
  print()
}

执行结果如下:

果然只打印了一次!了解这个思路后,现在我们要去收集传入的多种类型。比如,对于下面这段代码:

js 复制代码
function print(type) {
  console.log(type)
}

print("a")
print("b")
print("c")
print("a")

虽然 print 调用了 4 次,但是我们期望最后只打印一次,且结果为 ["a", "b", "c"]

原方法是对传入不同的回调函数进行收集,而现在,已知每次调用的都是同一个函数,首先就想到:要基于回调函数进行封装处理,这个回调函数作为一个入参传入。其次,收集传入的不同类型,原有的任务队列变成了已收集类型的集合。基于这个思路,我们需要进行一些改造,改造后的代码如下:

js 复制代码
const collectTypes = function (callback) {
  const types = new Set()
  const p = Promise.resolve()
  let isFlushing = false

  function flush() {
    if (isFlushing) return
    isFlushing = true
    p.then(() => {
      const typeValues = [...types.values()]
      callback(typeValues)
    }).finally(() => {
      isFlushing = false
      types.clear()
    })
  }

  return function (type) {
    types.add(type)
    flush()
  }
}
  • collectTypes 接收一个函数作为入参,同时执行完后返回一个新函数。
  • 新函数每次执行时,会将传入的 type 收集在内部的 types 中。
  • promise 实例 pthen 方法中处理最终结果,执行 callback 并将收集到的所有类型传入,同时在 finally 方法中清空 types

用法如下:

js 复制代码
const print = collectTypes(type => {
  console.log(type)
})

print("a")
print("b")
print("c")
print("a")

执行结果如下:

完美符合预期!

发起请求

收集到所有类型后,接下来就是发起请求。为了更具通用性,这个请求方法一定是可灵活配置的,也即它也是一个入参,可以根据需要自定义传入。其次,返回的新函数,要有一个返回值,返回对应类型的数据,这里先暂时返回请求到的所有类型的数据。改造后的代码如下:

js 复制代码
const mergeSimilarRequest = function (request) {
  const types = new Set()
  let p = Promise.resolve()
  let isFlushing = false

  function flush() {
    if (isFlushing) return
    isFlushing = true
    p = p
      .then(() => {
        const values = [...types.values()]
        return request(values)
      })
      .finally(() => {
        isFlushing = false
        types.clear()
      })
  }
  return function fetchData(type) {
    types.add(type)
    flush()
    return p
  }
}
  • 换一个更符合气质的名字:mergeSimilarRequest ------ 合并相似请求
  • 入参调整为 request(发起请求的方法),在最后处理阶段,将所有的类型传给 request,把获取数据的过程交给 request,只需要它的返回结果即可。
  • 刷新队列时,对 p 进行重新赋值,p 的结果即为 request 的执行结果(请求的所有数据)。
  • 返回的函数 fetchData 的执行结果:返回当前的 p 实例。

使用如下:

js 复制代码
const data = [
  { type: "a", name: "a-1" },
  { type: "a", name: "a-2" },
  { type: "b", name: "b-1" },
  { type: "b", name: "b-2" },
  { type: "c", name: "c-1" },
  { type: "c", name: "c-2" }
]

const request = types => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("发起请求,types:", types)
      const res = data.filter(v => types.includes(v.type))
      resolve(res)
    }, 1000)
  })
}

const fetchData = mergeSimilarRequest(request)

fetchData("a").then(dataA => {
  console.log("dataA", dataA)
})
fetchData("b").then(dataB => {
  console.log("dataB", dataB)
})

执行结果如下:

从执行结果可以看出:request 只执行了一次,并且入参的请求类型是 abdataAdataB 都是请求到的所有数据,还需要进行拆分。

拆分数据

最后一步,点睛之笔!现在已经知道实例 p 返回的是所有数据,而返回函数 fetchData 的入参里有 type,那么去做数据拆分就十分容易了。代码如下:

js 复制代码
const mergeSimilarRequest = function (request) {
  /* ... */
  return function fetchData(type) {
    /* ... */
    return p.then(res => {
      return res.filter(v => v.type === type)
    })
  }
}

但是,我们还需要考虑通用性的问题,并不是所有的类型字段就叫做 type,也有可能叫做 type1type2,返回的结果 res 也不一定都是数组类型。所以,不妨把这个拆分规则抽离成一个方法 filterRule ,作为 mergeSimilarRequest 的新入参传入。如下所示:

js 复制代码
const mergeSimilarRequest = function (request, filterRule) {
  /* ... */
  return function fetchData(type) {
    /* ... */
    return p.then(res => {
      return filterRule(res, type)
    })
  }
}

后续测试的时候发现,requestfilterRule 其实是强关联的,filterRule 的内部逻辑完全依赖于 request 返回数据的类型,于是就把它俩合并成一个入参,最终版代码如下:

js 复制代码
const mergeSimilarRequest = function ({ request, filterRule }) {
  const types = new Set()
  let p = Promise.resolve()
  let isFlushing = false

  function flush() {
    if (isFlushing) return
    isFlushing = true
    p = p
      .then(() => {
        const values = [...types.values()]
        return request(values)
      })
      .finally(() => {
        isFlushing = false
        types.clear()
      })
  }
  return function fetchData(type) {
    types.add(type)
    flush()
    return p.then(res => {
      return filterRule(res, type)
    })
  }
}

最后,测试一下:

js 复制代码
const data = [
  { type: "a", name: "a-1" },
  { type: "a", name: "a-2" },
  { type: "b", name: "b-1" },
  { type: "b", name: "b-2" },
  { type: "c", name: "c-1" },
  { type: "c", name: "c-2" }
]

const request = types => {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("发起请求,types:", types)
      const res = data.filter(v => types.includes(v.type))
      resolve(res)
    }, 1000)
  })
}
const filterRule = (data, type) => {
  return data.filter(v => v.type === type)
}
const config = {
  request,
  filterRule
}

const fetchData = mergeSimilarRequest(config)

fetchData("a").then(dataA => {
  console.log("dataA", dataA)
})
fetchData("b").then(dataB => {
  console.log("dataB", dataB)
})

执行结果如下:

Goooooooooooooooooooooooooooood ~ ~ ~ ~ ~ ~ ~

相关推荐
bloxed29 分钟前
前端文件下载多方式集合
前端·filedownload
余生H35 分钟前
前端Python应用指南(三)Django vs Flask:哪种框架适合构建你的下一个Web应用?
前端·python·django
LUwantAC44 分钟前
CSS(四)display和float
前端·css
cwtlw1 小时前
CSS学习记录20
前端·css·笔记·学习
界面开发小八哥1 小时前
「Java EE开发指南」如何用MyEclipse构建一个Web项目?(一)
java·前端·ide·java-ee·myeclipse
谢道韫6661 小时前
今日总结 2024-12-24
javascript·vue.js·elementui
一朵好运莲1 小时前
React引入Echart水球图
开发语言·javascript·ecmascript
米奇妙妙wuu1 小时前
react使用sse流实现chat大模型问答,补充css样式
前端·css·react.js
傻小胖1 小时前
React 生命周期完整指南
前端·react.js
梦境之冢2 小时前
axios 常见的content-type、responseType有哪些?
前端·javascript·http