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
}
复制代码