React SWR 庫是由開發Next.js的同一團隊Vercel開源出來的一款工具。其功能主要是用來實現HTTP RFC 5861規範中名爲stale-while-revalidate的緩存失效策略。簡單來講,就是可以在獲取數據的時候能夠先從緩存中返回數據,而後再發送請求進行驗證,最後更新數據的效果。從而達到能夠提早更新UI的目的。在低網速、緩存可用的狀況下,能夠提高用戶體驗。 接下來的這篇文章,主要是對其源碼進行一些分析和學習。javascript
swr
這個庫在使用過程當中,咱們主要是使用 useSWR
這個接口。java
useSWR
接口的輸入主要由如下參數組成:react
key: 用來標識緩存的key值,字符串或返回字符串的方法git
fetcher: 請求數據接口github
options: 配置參數,大頭, 具體參數以下typescript
suspense = false
: enable React Suspense mode (details)fetcher = window.fetch
: the default fetcher functioninitialData
: initial data to be returned (note: This is per-hook)revalidateOnMount
: enable or disable automatic revalidation when component is mounted (by default revalidation occurs on mount when initialData is not set, use this flag to force behavior)revalidateOnFocus = true
: auto revalidate when window gets focusedrevalidateOnReconnect = true
: automatically revalidate when the browser regains a network connection (vianavigator.onLine
)refreshInterval = 0
: polling interval (disabled by default)refreshWhenHidden = false
: polling when the window is invisible (ifrefreshInterval
is enabled)refreshWhenOffline = false
: polling when the browser is offline (determined bynavigator.onLine
)shouldRetryOnError = true
: retry when fetcher has an error (details)dedupingInterval = 2000
: dedupe requests with the same key in this time spanfocusThrottleInterval = 5000
: only revalidate once during a time spanloadingTimeout = 3000
: timeout to trigger the onLoadingSlow eventerrorRetryInterval = 5000
: error retry interval (details)errorRetryCount
: max error retry count (details)onLoadingSlow(key, config)
: callback function when a request takes too long to load (seeloadingTimeout
)onSuccess(data, key, config)
: callback function when a request finishes successfullyonError(err, key, config)
: callback function when a request returns an erroronErrorRetry(err, key, config, revalidate, revalidateOps)
: handler for error retrycompare(a, b)
: comparison function used to detect when returned data has changed, to avoid spurious rerenders. By default, dequal/lite is used.isPaused()
: function to detect whether pause revalidations, will ignore fetched data and errors when it returnstrue
. Returnsfalse
by default.api
輸出主要有如下幾個數據:數組
data: 數據promise
error: 錯誤信息緩存
isValidating: 請求是否在進行中
mutate(data, shouldRevalidate): 更改緩存數據的接口
先來看一下具體的使用方式:
import useSWR from 'swr'
function Profile() {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
複製代碼
其基本使用方式和日常的react hook是同樣的,經過傳遞一個做爲key的字符串和對應的fetcher接口來獲取對應的數據。
瞭解了使用方式後,接下來來查看一下具體的代碼實現。 經過查看源碼,總體實現流程能夠分爲如下幾個步驟:
配置config: 此步驟主要用來處理用戶輸入,將其轉換成內部須要用到的處理參數。
先從cache獲取數據, 內存保存一個ref引用對象,用來指向上次的請求接口(輸入中的key跟請求引用進行綁定)。若是緩存更新或key更新,則須要從新獲取數據。
處理請求操做,並暴露對外接口。
function useSWR<Data = any, Error = any>( ...args: | readonly [Key] | readonly [Key, Fetcher<Data> | null] | readonly [Key, SWRConfiguration<Data, Error> | undefined] | readonly [ Key, Fetcher<Data> | null, SWRConfiguration<Data, Error> | undefined ] ): SWRResponse<Data, Error> {
// 處理參數,並序列化對應的key信息
const [_key, config, fn] = useArgs<Key, SWRConfiguration<Data, Error>, Data>(
args
)
const [key, fnArgs, keyErr, keyValidating] = cache.serializeKey(_key)
// 保存引用
const initialMountedRef = useRef(false)
const unmountedRef = useRef(false)
const keyRef = useRef(key)
// 此處爲從緩存中獲取數據,若是緩存中沒有對應數據,則從配置的initialData中獲取
const resolveData = () => {
const cachedData = cache.get(key)
return cachedData === undefined ? config.initialData : cachedData
}
const data = resolveData()
const error = cache.get(keyErr)
const isValidating = resolveValidating()
// 中間省略,主要爲方法定義邏輯
// 在組件加載或key變化時觸發數據的更新邏輯,並添加一些事件監聽函數
useIsomorphicLayoutEffect(() => {
//... 省略
}, [key, revalidate])
// 輪詢處理,主要用以處理參數中的一些輪詢配置
useIsomorphicLayoutEffect(() => {}, [
config.refreshInterval,
config.refreshWhenHidden,
config.refreshWhenOffline,
revalidate
])
// 錯誤處理
if (config.suspense && data === undefined) {
if (error === undefined) {
throw revalidate({ dedupe: true })
}
throw error
}
// 最後返回狀態信息, 此處邏輯見狀態管理部分
}
複製代碼
對於用戶輸入部分,defaultConfig + useContext + 用戶自定義config 優先級關係爲: defaultConfig < useContext < 用戶自定義config
export default function useArgs<KeyType, ConfigType, Data>( args: | readonly [KeyType] | readonly [KeyType, Fetcher<Data> | null] | readonly [KeyType, ConfigType | undefined] | readonly [KeyType, Fetcher<Data> | null, ConfigType | undefined] ): [KeyType, (typeof defaultConfig) & ConfigType, Fetcher<Data> | null] {
// 此處用來處理config等參數
const config = Object.assign(
{},
defaultConfig,
useContext(SWRConfigContext),
args.length > 2
? args[2]
: args.length === 2 && typeof args[1] === 'object'
? args[1]
: {}
) as (typeof defaultConfig) & ConfigType
複製代碼
revalidate 從新更新數據, 在組件加載後或者當前狀態處於空閒時, 會從新更新數據。 須要處理depupe: 消重邏輯,即在短期內相同的請求須要進行去重。 聲明瞭一個CONCURRENT_PROMISES變量用來保存全部須要並行的請求操做。
const revalidate = useCallback(
async (revalidateOpts: RevalidatorOptions = {}): Promise<boolean> => {
if (!key || !fn) return false
if (unmountedRef.current) return false
if (configRef.current.isPaused()) return false
const { retryCount = 0, dedupe = false } = revalidateOpts
let loading = true
let shouldDeduping =
typeof CONCURRENT_PROMISES[key] !== 'undefined' && dedupe
try {
cache.set(keyValidating, true)
setState({
isValidating: true
})
if (!shouldDeduping) {
broadcastState(
key,
stateRef.current.data,
stateRef.current.error,
true
)
}
let newData: Data
let startAt: number
if (shouldDeduping) {
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
if (config.loadingTimeout && !cache.get(key)) {
setTimeout(() => {
if (loading)
safeCallback(() => configRef.current.onLoadingSlow(key, config))
}, config.loadingTimeout)
}
if (fnArgs !== null) {
CONCURRENT_PROMISES[key] = fn(...fnArgs)
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
CONCURRENT_PROMISES_TS[key] = startAt = now()
newData = await CONCURRENT_PROMISES[key]
setTimeout(() => {
if (CONCURRENT_PROMISES_TS[key] === startAt) {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}
}, config.dedupingInterval)
safeCallback(() => configRef.current.onSuccess(newData, key, config))
}
if (CONCURRENT_PROMISES_TS[key] !== startAt) {
return false
}
if (
MUTATION_TS[key] !== undefined &&
(startAt <= MUTATION_TS[key] ||
startAt <= MUTATION_END_TS[key] ||
MUTATION_END_TS[key] === 0)
) {
setState({ isValidating: false })
return false
}
// 設置緩存
cache.set(keyErr, undefined)
cache.set(keyValidating, false)
const newState: State<Data, Error> = {
isValidating: false
}
if (stateRef.current.error !== undefined) {
newState.error = undefined
}
if (!config.compare(stateRef.current.data, newData)) {
newState.data = newData
}
if (!config.compare(cache.get(key), newData)) {
cache.set(key, newData)
}
// merge the new state
setState(newState)
if (!shouldDeduping) {
// also update other hooks
broadcastState(key, newData, newState.error, false)
}
} catch (err) {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
if (configRef.current.isPaused()) {
setState({
isValidating: false
})
return false
}
// 從緩存中獲取錯誤信息
cache.set(keyErr, err)
if (stateRef.current.error !== err) {
setState({
isValidating: false,
error: err
})
if (!shouldDeduping) {
// 廣播狀態
broadcastState(key, undefined, err, false)
}
}
// events and retry
safeCallback(() => configRef.current.onError(err, key, config))
if (config.shouldRetryOnError) {
// 重試機制,須要容許消重
safeCallback(() =>
configRef.current.onErrorRetry(err, key, config, revalidate, {
retryCount: retryCount + 1,
dedupe: true
})
)
}
}
loading = false
return true
},
[key]
)
複製代碼
另外,mutate接口是對外輸出的一個讓用戶顯式調用來觸發從新更新數據的接口。好比用戶從新登陸的時候,須要顯式從新更新全部數據,此時就能夠使用 mutate
接口。其實現邏輯以下:
async function mutate<Data = any>( _key: Key, _data?: Data | Promise<Data | undefined> | MutatorCallback<Data>, shouldRevalidate = true ): Promise<Data | undefined> {
const [key, , keyErr] = cache.serializeKey(_key)
if (!key) return undefined
// if there is no new data to update, let's just revalidate the key
if (typeof _data === 'undefined') return trigger(_key, shouldRevalidate)
// update global timestamps
MUTATION_TS[key] = now() - 1
MUTATION_END_TS[key] = 0
// 追蹤時間戳
const beforeMutationTs = MUTATION_TS[key]
let data: any, error: unknown
let isAsyncMutation = false
if (typeof _data === 'function') {
// `_data` is a function, call it passing current cache value
try {
_data = (_data as MutatorCallback<Data>)(cache.get(key))
} catch (err) {
// if `_data` function throws an error synchronously, it shouldn't be cached
_data = undefined
error = err
}
}
if (_data && typeof (_data as Promise<Data>).then === 'function') {
// `_data` is a promise
isAsyncMutation = true
try {
data = await _data
} catch (err) {
error = err
}
} else {
data = _data
}
const shouldAbort = (): boolean | void => {
// check if other mutations have occurred since we've started this mutation
if (beforeMutationTs !== MUTATION_TS[key]) {
if (error) throw error
return true
}
}
// if there's a race we don't update cache or broadcast change, just return the data
if (shouldAbort()) return data
if (data !== undefined) {
// update cached data
cache.set(key, data)
}
// always update or reset the error
cache.set(keyErr, error)
// 重置時間戳,以代表更新完成
MUTATION_END_TS[key] = now() - 1
if (!isAsyncMutation) {
// we skip broadcasting if there's another mutation happened synchronously
if (shouldAbort()) return data
}
// 更新階段
const updaters = CACHE_REVALIDATORS[key]
if (updaters) {
const promises = []
for (let i = 0; i < updaters.length; ++i) {
promises.push(
updaters[i](!!shouldRevalidate, data, error, undefined, i > 0)
)
}
// 返回更新後的數據
return Promise.all(promises).then(() => {
if (error) throw error
return cache.get(key)
})
}
// 錯誤處理
if (error) throw error
return data
}
複製代碼
對於緩存的增刪改查,swr源碼中專門對其作了個封裝,並採用訂閱—發佈模式監聽緩存的操做。 下面是cache文件: //cache.ts 文件
import { Cache as CacheType, Key, CacheListener } from './types'
import hash from './libs/hash'
export default class Cache implements CacheType {
private cache: Map<string, any>
private subs: CacheListener[]
constructor(initialData: any = {}) {
this.cache = new Map(Object.entries(initialData))
this.subs = []
}
get(key: Key): any {
const [_key] = this.serializeKey(key)
return this.cache.get(_key)
}
set(key: Key, value: any): any {
const [_key] = this.serializeKey(key)
this.cache.set(_key, value)
this.notify()
}
keys() {
return Array.from(this.cache.keys())
}
has(key: Key) {
const [_key] = this.serializeKey(key)
return this.cache.has(_key)
}
clear() {
this.cache.clear()
this.notify()
}
delete(key: Key) {
const [_key] = this.serializeKey(key)
this.cache.delete(_key)
this.notify()
}
// TODO: introduce namespace for the cache
serializeKey(key: Key): [string, any, string, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}
if (Array.isArray(key)) {
// args array
args = key
key = hash(key)
} else {
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
const isValidatingKey = key ? 'validating@' + key : ''
return [key, args, errorKey, isValidatingKey]
}
subscribe(listener: CacheListener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
let isSubscribed = true
this.subs.push(listener)
return () => {
if (!isSubscribed) return
isSubscribed = false
const index = this.subs.indexOf(listener)
if (index > -1) {
this.subs[index] = this.subs[this.subs.length - 1]
this.subs.length--
}
}
}
private notify() {
for (let listener of this.subs) {
listener()
}
}
}
複製代碼
從源碼上能夠看到,當緩存更新的時候,會觸發內部 notify
接口通知到全部訂閱了相關更新的處理函數,從而能夠更好地監聽到數據的變化。
SWR對外暴露的狀態是以響應式的方式進行處理,以便在後續數據更新的時候能觸發組件的自動更新。 具體代碼以下:
// 帶引用的狀態數據,在後續依賴更新時,將會自動觸發render
export default function useStateWithDeps<Data, Error, S = State<Data, Error>>( state: S, unmountedRef: MutableRefObject<boolean> ): [ MutableRefObject<S>, MutableRefObject<Record<StateKeys, boolean>>, (payload: S) => void ] {
// 此處聲明一個空對象的狀態,獲取其setState,而後在後續須要從新渲染的時候,調用該方法。
const rerender = useState<object>({})[1]
const stateRef = useRef(state)
useIsomorphicLayoutEffect(() => {
stateRef.current = state
})
// 若是一個狀態屬性在組件的渲染函數中被訪問到,就須要在內部將其做爲依賴標記下來,以便在後續這些狀態數據更新的時候,可以觸發重渲染。
const stateDependenciesRef = useRef<StateDeps>({
data: false,
error: false,
isValidating: false
})
/* 使用setState顯式的方式去觸發狀態更新 */
const setState = useCallback(
(payload: S) => {
let shouldRerender = false
for (const _ of Object.keys(payload)) {
// Type casting to work around the `for...in` loop
// [https://github.com/Microsoft/TypeScript/issues/3500](https://github.com/Microsoft/TypeScript/issues/3500)
const k = _ as keyof S & StateKeys
// If the property hasn't changed, skip
if (stateRef.current[k] === payload[k]) {
continue
}
stateRef.current[k] = payload[k]
// 若是屬性被外部組件訪問過,則會觸發從新渲染
if (stateDependenciesRef.current[k]) {
shouldRerender = true
}
}
if (shouldRerender && !unmountedRef.current) {
rerender({})
}
},
// config.suspense isn't allowed to change during the lifecycle
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
return [stateRef, stateDependenciesRef, setState]
}
function useSWR<Data = any, Error = any>( ...args: | readonly [Key] | readonly [Key, Fetcher<Data> | null] | readonly [Key, SWRConfiguration<Data, Error> | undefined] | readonly [ Key, Fetcher<Data> | null, SWRConfiguration<Data, Error> | undefined ] ): SWRResponse<Data, Error> {
// 。。。。
const [stateRef, stateDependenciesRef, setState] = useStateWithDeps<
Data,
Error
>(
{
data,
error,
isValidating
},
unmountedRef
)
//...
// 最終返回的狀態,是作了響應式包裝的數據,當訪問狀態數據的時候,會更新依賴
const state = {
revalidate,
mutate: boundMutate
} as SWRResponse<Data, Error>
Object.defineProperties(state, {
data: {
get: function() {
stateDependenciesRef.current.data = true
return data
},
enumerable: true
},
error: {
get: function() {
stateDependenciesRef.current.error = true
return error
},
enumerable: true
},
isValidating: {
get: function() {
stateDependenciesRef.current.isValidating = true
return isValidating
},
enumerable: true
}
})
return state
}
複製代碼