React SWR 库是由开发Next.js的同一团队Vercel开源出来的一款工具。其功能主要是用来实现HTTP RFC 5861规范中名为stale-while-revalidate的缓存失效策略。简单来说,就是能够在获取数据的时候可以先从缓存中返回数据,然后再发送请求进行验证,最后更新数据的效果。从而达到可以提前更新UI的目的。在低网速、缓存可用的情况下,可以提升用户体验。
接下来的这篇文章,主要是对其源码进行一些分析和学习。
认识一下接口
swr
这个库在使用过程中,我们主要是使用 useSWR
这个接口。
输入
useSWR
接口的输入主要由以下参数组成:
-
key: 用来标识缓存的key值,字符串或返回字符串的方法
-
fetcher: 请求数据接口
-
options: 配置参数,大头, 具体参数如下
suspense = false
: enable React Suspense mode (details)
fetcher = window.fetch
: the default fetcher function
initialData
: 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 focused
revalidateOnReconnect = 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 span
focusThrottleInterval = 5000
: only revalidate once during a time span
loadingTimeout = 3000
: timeout to trigger the onLoadingSlow event
errorRetryInterval = 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 successfully
onError(err, key, config)
: callback function when a request returns an error
onErrorRetry(err, key, config, revalidate, revalidateOps)
: handler for error retry
compare(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.
输出
输出主要有以下几个数据:
-
data: 数据
-
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
}
// 最后返回状态信息, 此处逻辑见状态管理部分
}
复制代码
config的逻辑
对于用户输入部分,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
}
复制代码