【你不知道的 promise】設計一個支持併發的前端接口緩存
封裝一下調用接口的方法,調用時先走咱們緩存數據。
import axios, { AxiosRequestConfig } from 'axios'
// 先來一個簡簡單單的發送
export function sendRequest(request: AxiosRequestConfig) {
return axios(request)
}
然後加上咱們的緩存
import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'
const cacheMap = new Map()
interface MyRequestConfig extends AxiosRequestConfig {
needCache?: boolean
}
// 這裏用params是因爲params是 GET 方式穿的參數,我們的緩存一般都是 GET 接口用的
function generateCacheKey(config: MyRequestConfig) {
return config.url + '?' + qs.stringify(config.params)
}
export function sendRequest(request: MyRequestConfig) {
const cacheKey = generateCacheKey(request)
// 判斷是否需要緩存,並且緩存池中有值時,返回緩存池中的值
if (request.needCache && cacheMap.has(cacheKey)) {
return Promise.resolve(cacheMap.get(cacheKey))
}
return axios(request).then((res) => {
// 這裏簡單判斷一下,200就算成功了,不管裏面的data的code啥的了
if (res.status === 200) {
cacheMap.set(cacheKey, res.data)
}
return res
})
}
然後調用
const getArticleList = (params: any) =>
sendRequest({
needCache: true,
url: '/article/list',
method: 'get',
params
})
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
這個部分就很簡單,我們在調接口時給一個needCache
的標記,然後調完接口如果成功的話,就會將數據放到cacheMap
中去,下次再調用的話,就直接返回緩存中的數據。
併發緩存
上面的雖然看似實現了緩存,不管我們調用幾次,都只會發送一次請求,剩下的都會走緩存。但是真的是這樣嗎?
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
其實這樣,就可以測出,我們的雖然設計了緩存,但是請求還是發送了兩次,這是因爲我們第二次請求發出時,第一次請求還沒完成,也就沒給緩存池裏放數據,所以第二次請求沒命中緩存,也就又發了一次。
問題
那麼,有沒有一種辦法讓第二次請求
等待第一次請求調用完成,然後再一塊返回呢?
思考
有了!我們寫個定時器就好了呀,比如我們可以給第二次請求加個定時器,定時器時間到了再去cacheMap
中查一遍有沒有緩存數據,沒有的話可能是第一個請求還沒好,再等幾秒試試!
可是這樣的話,第一個請求的時候也會在原地等呀!😒
那這樣的話,讓第一個請求
在一個地方貼個告示不就好了,就像上廁所的時候在門口掛個牌子一樣!😎
// 存儲緩存當前狀態,相當於掛牌子的地方
const statusMap = new Map<string, 'pending' | 'complete'>();
export function sendRequest(request: MyRequestConfig) {
const cacheKey = generateCacheKey(request)
// 判斷是否需要緩存
if (request.needCache) {
if (statusMap.has(cacheKey)) {
const currentStatus = statusMap.get(cacheKey)
// 判斷當前的接口緩存狀態,如果是 complete ,則代表緩存完成
if (currentStatus === 'complete') {
return Promise.resolve(cacheMap.get(cacheKey))
}
// 如果是 pending ,則代表正在請求中,這裏就等個三秒,然後再來一次看看情況
if (currentStatus === 'pending') {
return new Promise((resolve, reject) => {
setTimeout(() => {
sendRequest(request).then(resolve, reject)
}, 3000)
})
}
}
statusMap.set(cacheKey, 'pending')
}
return axios(request).then((res) => {
// 這裏簡單判斷一下,200就算成功了,不管裏面的data的code啥的了
if (res.status === 200) {
statusMap.set(cacheKey, 'complete')
cacheMap.set(cacheKey, res)
}
return res
})
}
試試效果
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
成了!這裏真的做到了,可以看到我們這裏打印了兩次,但是隻發了一次請求。
優化🤔
可是用setTimeout
等待還是不太優雅,如果第一個請求能在3s
以內完成還行,用戶等待的時間還不算太久,還能忍受。可如果是3.1s
的話,第二個接口用戶可就白白等了6s
之久,那麼,有沒有一種辦法,能讓第一個接口
完成後,接着就通知第二個接口
返回數據呢?
等待,通知,這種場景我們寫代碼用的最多的就是回調了,但是這次用的是promise
啊,而且還是毫不相干的兩個promise
。 等等!callback
和promise
,promise
本身就是callback
實現的!promise
的then
會在resole
被調用時調用,這樣的話,我們可以將第二個請求的resole
放在一個callback
裏,然後在第一個請求完成的時候,調用這個callback
!🥳
// 定義一下回調的格式
interface RequestCallback {
onSuccess: (data: any) => void
onError: (error: any) => void
}
// 存放等待狀態的請求回調
const callbackMap = new Map<string, RequestCallback[]>()
export function sendRequest(request: MyRequestConfig) {
const cacheKey = generateCacheKey(request)
// 判斷是否需要緩存
if (request.needCache) {
if (statusMap.has(cacheKey)) {
const currentStatus = statusMap.get(cacheKey)
// 判斷當前的接口緩存狀態,如果是 complete ,則代表緩存完成
if (currentStatus === 'complete') {
return Promise.resolve(cacheMap.get(cacheKey))
}
// 如果是 pending ,則代表正在請求中,這裏放入回調函數
if (currentStatus === 'pending') {
return new Promise((resolve, reject) => {
if (callbackMap.has(cacheKey)) {
callbackMap.get(cacheKey)!.push({
onSuccess: resolve,
onError: reject
})
} else {
callbackMap.set(cacheKey, [
{
onSuccess: resolve,
onError: reject
}
])
}
})
}
}
statusMap.set(cacheKey, 'pending')
}
return axios(request).then(
(res) => {
// 這裏簡單判斷一下,200就算成功了,不管裏面的data的code啥的了
if (res.status === 200) {
statusMap.set(cacheKey, 'complete')
cacheMap.set(cacheKey, res)
} else {
// 不成功的情況下刪掉 statusMap 中的狀態,能讓下次請求重新請求
statusMap.delete(cacheKey)
}
// 這裏觸發resolve的回調函數
if (callbackMap.has(cacheKey)) {
callbackMap.get(cacheKey)!.forEach((callback) => {
callback.onSuccess(res)
})
// 調用完成之後清掉,用不到了
callbackMap.delete(cacheKey)
}
return res
},
(error) => {
// 不成功的情況下刪掉 statusMap 中的狀態,能讓下次請求重新請求
statusMap.delete(cacheKey)
// 這裏觸發reject的回調函數
if (callbackMap.has(cacheKey)) {
callbackMap.get(cacheKey)!.forEach((callback) => {
callback.onError(error)
})
// 調用完成之後也清掉
callbackMap.delete(cacheKey)
}
// 這裏要返回 Promise.reject(error),才能被catch捕捉到
return Promise.reject(error)
}
)
}
在判斷到當前請求狀態是pending
時,將promise
的resole
與reject
放入回調隊列中,等待被觸發調用。 然後在請求完成時,觸發對應的請求隊列。
試一下
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
getArticleList({
page: 1,
pageSize: 10
}).then((res) => {
console.log(res)
})
OK,完成了。
再試一下失敗的時候
getArticleList({
page: 1,
pageSize: 10
}).then(
(res) => {
console.log(res)
},
(error) => {
console.error(error)
}
)
getArticleList({
page: 1,
pageSize: 10
}).then(
(res) => {
console.log(res)
},
(error) => {
console.error(error)
}
)
OK,兩個都失敗了。(但是這裏的 error2 早於 error1 打印,你知道是啥原因嗎?🤔)
總結
promise
封裝併發緩存到這裏就結束啦,不過看到這裏你可能會覺着沒啥用處,但是其實這也是我碰到的一個需求才延申出來的,當時的場景是一個頁面裏有好幾個下拉選擇框,選項都是接口提供的常量。但是隻接口提供了一個接口返回這些常量,前端拿到以後自己再根據類型挑出來,所以這種情況我們肯定不能每個下拉框都去調一次接口,只能是寄託緩存機制了。
這種寫法,在另一種場景下也很好用,比如將需要用戶操作的流程封裝成promise
。例如,A
頁面點擊A
按鈕,出現一個B
彈窗,彈窗裏有B
按鈕,用戶點擊B
按鈕之後關閉彈窗,再彈出C
彈窗C
按鈕,點擊C
之後流程完成,這種情況就很適合將每個彈窗裏的操作流程都封裝成一個promise
,最外面的A
頁面只需要連着調用這幾個promise
就可以了,而不需要維護控制這幾個彈窗顯示隱藏的變量了。
放一下全部代碼
import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'
// 存儲緩存數據
const cacheMap = new Map()
// 存儲緩存當前狀態
const statusMap = new Map<string, 'pending' | 'complete'>()
// 定義一下回調的格式
interface RequestCallback {
onSuccess: (data: any) => void
onError: (error: any) => void
}
// 存放等待狀態的請求回調
const callbackMap = new Map<string, RequestCallback[]>()
interface MyRequestConfig extends AxiosRequestConfig {
needCache?: boolean
}
// 這裏用params是因爲params是 GET 方式穿的參數,我們的緩存一般都是 GET 接口用的
function generateCacheKey(config: MyRequestConfig) {
return config.url + '?' + qs.stringify(config.params)
}
export function sendRequest(request: MyRequestConfig) {
const cacheKey = generateCacheKey(request)
// 判斷是否需要緩存
if (request.needCache) {
if (statusMap.has(cacheKey)) {
const currentStatus = statusMap.get(cacheKey)
// 判斷當前的接口緩存狀態,如果是 complete ,則代表緩存完成
if (currentStatus === 'complete') {
return Promise.resolve(cacheMap.get(cacheKey))
}
// 如果是 pending ,則代表正在請求中,這裏放入回調函數
if (currentStatus === 'pending') {
return new Promise((resolve, reject) => {
if (callbackMap.has(cacheKey)) {
callbackMap.get(cacheKey)!.push({
onSuccess: resolve,
onError: reject
})
} else {
callbackMap.set(cacheKey, [
{
onSuccess: resolve,
onError: reject
}
])
}
})
}
}
statusMap.set(cacheKey, 'pending')
}
return axios(request).then(
(res) => {
// 這裏簡單判斷一下,200就算成功了,不管裏面的data的code啥的了
if (res.status === 200) {
statusMap.set(cacheKey, 'complete')
cacheMap.set(cacheKey, res)
} else {
// 不成功的情況下刪掉 statusMap 中的狀態,能讓下次請求重新請求
statusMap.delete(cacheKey)
}
// 這裏觸發resolve的回調函數
if (callbackMap.has(cacheKey)) {
callbackMap.get(cacheKey)!.forEach((callback) => {
callback.onSuccess(res)
})
// 調用完成之後清掉,用不到了
callbackMap.delete(cacheKey)
}
return res
},
(error) => {
// 不成功的情況下刪掉 statusMap 中的狀態,能讓下次請求重新請求
statusMap.delete(cacheKey)
// 這裏觸發reject的回調函數
if (callbackMap.has(cacheKey)) {
callbackMap.get(cacheKey)!.forEach((callback) => {
callback.onError(error)
})
// 調用完成之後也清掉
callbackMap.delete(cacheKey)
}
return Promise.reject(error)
}
)
}
const getArticleList = (params: any) =>
sendRequest({
needCache: true,
baseURL: 'http://localhost:8088',
url: '/article/blogList',
method: 'get',
params
})
export function testApi() {
getArticleList({
page: 1,
pageSize: 10
}).then(
(res) => {
console.log(res)
},
(error) => {
console.error('error1:', error)
}
)
getArticleList({
page: 1,
pageSize: 10
}).then(
(res) => {
console.log(res)
},
(error) => {
console.error('error2:', error)
}
)
}
最後
對請求結果是否成功那裏處理的比較簡陋,項目裏用到的話根據自己情況來。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://juejin.cn/post/7104635370796482567