手把手教你寫React hook請求

SWR 做爲一個基於 react hook 的請求庫, 在剛推出時便一晚上爆火, 從目前 issue 的解決速度和功能來看, 是一個不錯的庫.前端

官方文檔: github.com/zeit/swr

本篇文章結合 API 深刻淺出了解SWR源碼的實現.react

PS: 本文閱讀大概須要18分鐘時間.如對細節不關心可直接查看「有意思的實現」 瞭解做者作的優化點.git

目錄github

  • 解析參數
  • 依賴處理
  • 全局配置
  • 請求數據邏輯
  • 回調處理
  • 循環請求
  • 緩存處理
  • 總結

解析參數

從 API 來看, 接收3個參數, 咱們再看看源碼算法

if (args.length >= 1) {
    _key = args[0]
}
if (typeof args[1] === 'function') {
    fn = args[1]   //若是第二個參數是函數, 則賦值給 fn 
} else if (typeof args[1] === 'object') {
    config = args[1] //若是第二個參數是對象, 則賦值給 config 
}
if (typeof args[2] === 'object') {
    config = args[2]
}
if (typeof fn === 'undefined') {
    fn = config.fetcher  //若沒傳 fn , 則使用默認配置的函數
}
複製代碼

從這段代碼能夠看出, 調用能夠傳 fetcher, 也能夠不傳 fetcher , 能夠將 fetcher 的參數做爲數組傳遞過去, 讓調用更加靈活. 也將請求和經常使用的處理邏輯分隔開來. 達到解耦的效果.api

const { data } = useSWR( key, fetcher, options)
const { data } = useSWR('/api/user', { refreshInterval: 0 }) 
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher) //條件取key值
const { data } = useSWR(() => '/api/projects?uid=' + id) //key 爲函數
useSWR(['/api/user', params], fetcher) // key 爲數組
複製代碼

第一個參數 key 能夠傳遞字符串, 能夠傳遞數組, 也能夠傳遞一個函數.數組

const getKeyArgs = key => {
  let args = null
  if (typeof key === 'function') {
    try {
      key = key()
    } catch (err) {
      // 若是拿到依賴的值仍未解析到, 會拋出錯誤,則表示依賴還未好
      key = ''
    }
  }
  if (Array.isArray(key)) {
    args = key
    key = hash(key)
  } else {
    key = String(key || '') // 若解析不到值, 則表示依賴未好或使用方式錯誤
  }

  return [key, args] // 解析到的參數做爲第二個函數的入參
}
複製代碼

tips: 直接用 key 值來作惟一的標識, 若是傳遞的數組中有對象,每次都會從新建立對象, 都是一個不同的值,key值發生變化,就會進入死循環. 解決方法是使用 useMemo, 以下:瀏覽器

const params = useMemo(() => ({ id }), [id])
useSWR(['/api/user', params], fetcer)
複製代碼

依賴處理

這裏有一個頗有意思的地方.一樣的, 咱們先看看 API. 第二個請求必定能夠保證在第一個請求結束後再發出.緩存

const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
複製代碼
有意思的實現

用短短几行代碼, 便實現了一個依賴請求. 在解析參數的時候, 若拿不到依賴的值, 則報錯進入catch流程. 以下:bash

if (typeof key === 'function') {
    try {
      key = key()
    } catch (err) {
      // 若是拿到依賴的值仍未解析到, 會拋出錯誤,則表示依賴還未好
      key = ''
    }
}
複製代碼

在處理獲取接口的函數裏. 若 key 爲 ‘’ , 則不作任何處理, 當依賴的接口返回了數據, 第二個接口的依賴值 key 發生了改變, 便從新觸發發起請求.

const revalidate = useCallback(async(revalidateOpts) => {
    if (!key) return false //沒有key值則直接返回
},[key]) // 當依賴項 key 變化時 useCallback 會從新執行
複製代碼

全局配置

接下來咱們看到配置參數的獲取和初始化, 這裏除了 swr 本身默認的 defaultConfig 以外, 還使用了 useContext .

const defaultConfig: ConfigInterface = {
  onLoadingSlow: () => {},
  onSuccess: () => {},
  onError: () => {},
  onErrorRetry,
  errorRetryInterval: 5 * 1000,
  focusThrottleInterval: 5 * 1000,
  dedupingInterval: 2 * 1000,
  loadingTimeout: 3 * 1000,
  refreshInterval: 0,
  revalidateOnFocus: true,
  refreshWhenHidden: false,
  shouldRetryOnError: true,
  suspense: false
}
config = Object.assign(
    {},
    defaultConfig,
    useContext(SWRConfigContext),
    config
)
複製代碼

其實按照以往寫公共組件的方法, 咱們可能會用類, 而後寫一個靜態 static 方法, 讓用戶調用這個靜態方法, 全局初始化數據. 但 swr 明顯不可能使用 class , 這裏使用了 Context 來配置全局數據共享.
咱們依然結合他的 api 來看.

import useSWR, { SWRConfig } from 'swr'
function App () {
  return (
    <SWRConfig 
      value={{
        refreshInterval: 3000,
      }}
    >
      組件..
    </SWRConfig>
  )
}
複製代碼

有意思的實現

從 swr 裏面引入了 SWRConfig ,那咱們找一下源碼裏面的內容. 對外暴露了一個 Provider, 外部直接接收一個 value 屬性, 內部使用 useContext(SWRConfigContext) 獲取對應的參數.

const SWRConfigContext = createContext<ConfigInterface>({}) 
SWRConfigContext.displayName = 'SWRConfigContext'
const SWRConfig = SWRConfigContext.Provider
export { SWRConfig }
複製代碼

小tip: 若本身嘗試封裝基於 hook 的組件, 通用的配置方式能夠參考這種方式.

請求數據邏輯

先列一個大概的輪廓, 核心的步驟以下:

  1. 將相關的請求處理放在 revalidate 中,用 useCallback 根據 key 值作緩存. key 值爲入參.
  2. 在 useIsomorphicLayoutEffect 時,如有緩存則優先使用緩存數據, 再異步調用並更新數據.
let [state, dispatch] = useReducer(mergeState, {
    data: initialData,
    error: initialError,
    isValidating: false
}) // 使用 reducer 的方式修改數據
const [key, fnArgs] = getKeyArgs(_key) //內部的代碼在參數解析部分有講
const unmountedRef = useRef(false)  //緩存 mounted 的狀態
const revalidate = useCallback(async(revalidateOpts) => {
    if (unmountedRef.current) return false
    try {
        // 請求超時觸發 onLoadingSlow 回調函數
        // 將請求記錄到 CONCURRENT_PROMISES 對象
        if (fnArgs !== null) {
            CONCURRENT_PROMISES[key] = fn(...fnArgs) //將傳的參數傳遞過去
        } else {
            CONCURRENT_PROMISES[key] = fn(key)
        }
        // 執行請求
        newData = await CONCURRENT_PROMISES[key]
        // 請求成功時的回調
        config.onSuccess(newData, key, config)
        // 將請求結果存儲到緩存 cache 中
        cacheSet(key, newData)
        // 對比新舊數據,若數據發生改變, 則批量改變數據
        if (deepEqual(dataRef.current, newData)) {} else {
            newState.data = newData
            dataRef.current = newData
        }
        dispatch(newState)
    }catch(err) {
        // 請求失敗設置值
        // 請求失敗時回調
        // 根據配置判斷是否重試請求
    }
},[key]) // 當依賴項 key 變化時 useCallback 會從新執行
useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect //根據服務端客戶端選擇 useEffect 仍是 useLayoutEffect
useIsomorphicLayoutEffect(() => {
    unmountedRef.current = false
    const softRevalidate = () => revalidate()
    if (
      typeof latestKeyedData !== 'undefined' &&
      window['requestIdleCallback']
    ) {
      // 若是有緩存則延遲從新驗證,優先使用緩存數據進行渲染
      window['requestIdleCallback'](softRevalidate)
    } else {
      softRevalidate()
    }
    
    // 窗口聚焦時,從新驗證
    // 註冊全局緩存的更新監聽函數
    // 根據配置的 refreshInterval 循環請求
    return () => {
        // 清除反作用
        unmountedRef.current = true
        ...
    }
}, [key, config.refreshInterval, revalidate])
return {
    error,
    data,
    revalidate,
    isValidating
}
複製代碼

有意思的實現

  1. 以前版本的 swr 使用的是 unstable_batchedUpdates 來進行批量處理. . 此處使用了 useReducer 來優化批量修改數據, 若無使用此方法, 會致使修改 loading, data 數據引發了2次渲染.
  2. 使用 useCallback 接收key值, 若key值不變, 則可使用上次的函數. 提升了性能.
  3. 使用 useRef 來存儲 unmountedRef 值, 在組件卸載的時候, 再也不 setState, 解決了組件卸載以後, 接口請求回來 setState 的錯誤.
  4. 使用 useLayoutEffect 在 ui 渲染前開始觸發請求, 稍微提升了速度. 而且兼容了服務端渲染的狀況.
  5. 請求函數被 requestIdleCallback 包裹, 非阻塞.
  6. 將發起請求的處理放在 revalidate 中, 方便回調及各類調用.

回調處理

在請求的時候, 咱們常常有一些定製化的需求, 好比「接口錯誤後須要展現什麼數據」, 「接口錯誤後重試多少次」, 若是每一個接口都須要寫一遍, 那真的太難受了. 一樣的, 咱們看一下調用的 API

useSWR(key, fetcher, {
  onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
    if (retryCount >= 10) return
    if (error.status === 404) return

    // 5秒後重試
    setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
  }
})
複製代碼

這塊的源碼很簡單. 就是在接口錯誤的時候, 調用傳遞進來的 onErrorRetry 函數.

if (config.shouldRetryOnError) {
    const retryCount = (revalidateOpts.retryCount || 0) + 1
    config.onErrorRetry(err,key,config, revalidate, Object.assign({ dedupe: true }, revalidateOpts, { retryCount })) // 默認設置 dedupe 爲 true , 配置能夠設置在指定時間內若是發起一樣的請求, 則不請求. 
}
複製代碼

這裏注意的是, 若你不重寫onErrorRetry函數,則按照它默認的方式(指數回退算法)來處理. emmm……源碼以下:

const count = Math.min(opts.retryCount || 0, 8);
const timeout =
  ~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval;
複製代碼

循環請求

跟 error 回調錯誤處理同樣, 這裏都是默認 dedupe 爲 true .

useIsomorphicLayoutEffect(() => {
const softRevalidate = () => revalidate({ dedupe: true })
let timeout = null
if (config.refreshInterval) {
    const tick = async () => {
        if (!errorRef.current && (config.refreshWhenHidden || isDocumentVisible())
        ) {
          // 頁面可視時, 且接口未錯誤時, 發起請求. //若是你想讓接口錯誤的時候也繼續循環, 那得在 onError 裏面處理
          await softRevalidate()
        }
        const interval = config.refreshInterval
        timeout = setTimeout(tick, interval)
    }
    timeout = setTimeout(tick, config.refreshInterval)
}
}, [key, config.refreshInterval, revalidate]) //循環參數發生變化時,從新執行
複製代碼

從上面基本的輪廓已經能夠看到是如何發起一個請求, 以及如何拿到數據. 倘若你的 hook 請求庫不須要作緩存, 使用上面的輪廓基本上就能夠知足業務需求了.

緩存處理

不過很明顯,這個庫主打的是 「stale-while-revalidate 」: 旨在經過緩存提升用戶體驗。其核心就是容許客戶端先使用緩存中不新鮮的數據,而後在後臺異步從新驗證更新緩存,等下次使用的時候數據就是新的了, 即在請求以前先從緩存返回數據(stale),而後在異步發送請求,最後當數據返回時更新緩存並觸發 UI 的從新渲染,從而提升用戶體驗。

有意思的實現

核心: 由於咱們須要使用到緩存(舊的數據),必然得有個變量來存儲接口返回的數據於內存中.

const initialData =
    (shouldReadCache ? cacheGet(key) : undefined) || config.initialData
const initialError = shouldReadCache ? cacheGet(keyErr) : undefined
const revalidate = useCallback(
    async () => {
    try {
        //發起請求
        const newData = await CONCURRENT_PROMISES[key] //存入併發的數組中, 可過參數dedupingInterval控制併發量
        // 將請求結果存儲到緩存 cache 中
        cacheSet(key, newData)
        cacheSet(keyErr, undefined)
        keyRef.current = key
    }catch(err) {
        cacheSet(keyErr, err)
        keyRef.current = key
    }
    }, [key])
useIsomorphicLayoutEffect(() => {
const latestKeyedData = cacheGet(key) || config.initialData
// 若是有最新的數據, 則等瀏覽器空閒的時候再從新發起請求. 
if (
      typeof latestKeyedData !== 'undefined' &&
      !IS_SERVER &&
      window['requestIdleCallback']
    ) {
      window['requestIdleCallback'](softRevalidate)
    } else {
      softRevalidate()
    }
}, [key, config.refreshInterval, revalidate]) 
複製代碼

最後

  • 歡迎加我微信(A18814127),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端加加」,認真學前端,作個有專業的技術人...

相關文章
相關標籤/搜索