前言
在日常开发中,有一种很常见的接口场景:例如,需要获取 type
为 a
的数据,这时候的接口入参为 { type: ["a"] }
。如果需要同时获取 type
为 a
和 b
的数据,则入参为 { 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()
}
毫无疑问会打印 10
次 print
。如果只想打印 1
次 print
,该如何实现?
不妨先了解一个小知识点: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
实例p
的then
方法中处理最终结果,执行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
只执行了一次,并且入参的请求类型是 a
和 b
,dataA
与 dataB
都是请求到的所有数据,还需要进行拆分。
拆分数据
最后一步,点睛之笔!现在已经知道实例 p
返回的是所有数据,而返回函数 fetchData
的入参里有 type
,那么去做数据拆分就十分容易了。代码如下:
js
const mergeSimilarRequest = function (request) {
/* ... */
return function fetchData(type) {
/* ... */
return p.then(res => {
return res.filter(v => v.type === type)
})
}
}
但是,我们还需要考虑通用性的问题,并不是所有的类型字段就叫做 type
,也有可能叫做 type1
、type2
,返回的结果 res
也不一定都是数组类型。所以,不妨把这个拆分规则抽离成一个方法 filterRule
,作为 mergeSimilarRequest
的新入参传入。如下所示:
js
const mergeSimilarRequest = function (request, filterRule) {
/* ... */
return function fetchData(type) {
/* ... */
return p.then(res => {
return filterRule(res, type)
})
}
}
后续测试的时候发现,request
和 filterRule
其实是强关联的,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 ~ ~ ~ ~ ~ ~ ~