【你不知道的 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。 等等!callbackpromisepromise本身就是callback實現的!promisethen會在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時,將promiseresolereject放入回調隊列中,等待被觸發調用。 然後在請求完成時,觸發對應的請求隊列。

試一下

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