SWR 做爲一個基於 react hook 的請求庫, 在剛推出時便一晚上爆火, 從目前 issue 的解決速度和功能來看, 是一個不錯的庫.前端
本篇文章結合 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 的組件, 通用的配置方式能夠參考這種方式.
先列一個大概的輪廓, 核心的步驟以下:
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
}
複製代碼
在請求的時候, 咱們常常有一些定製化的需求, 好比「接口錯誤後須要展現什麼數據」, 「接口錯誤後重試多少次」, 若是每一個接口都須要寫一遍, 那真的太難受了. 一樣的, 咱們看一下調用的 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])
複製代碼