授权转载自:JonyYu
https://github.com/forthealllight/blog/issues/61
swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目中如何实战使用swr,并且会解析一下swr的原理。从原理出发读一读swr的源码
什么是swr
swr的的源码
一、什么是swr
useSWR
是 react hooks 中一个比较有意思的组件,既可以作为请求库,也可以作为状态管理的缓存用,SWR 的名字来源于“stale-while-revalidate”, 是在HTTP RFC 5861标准中提出的一种缓存更新策略 :
首先从缓存中取数据,然后去真实请求相应的数据,最后将缓存值和最新值做对比,如果缓存值与最新值相同,则不用更新,否则用最新值来更新缓存,同时更新UI展示效果。
useSWR
可以作为请求库来用:
//fetch
import useSWR from 'swr'
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
//axios
const fetcher = url => axios.get(url).then(res => res.data)
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
//graphql
import { request } from 'graphql-request'
const fetcher = query => request('https://api.graph.cool/simple/v1/movies', query)
function App () {
const { data, error } = useSWR(
`{
Movie(title: "Inception") {
releaseDate
actors {
name
}
}
}`,
fetcher
)
// ...
}
此外,因为相同的 key
总是返回相同的实例,在 useSWR
中只保存了一个 cache
实例,因此 useSWR
也可以当作全局的状态管理机。比如可以全局保存用户名称 :
import useSWR from 'swr';
function useUser(id: string) {
const { data, error } = useSWR(`/api/user`, () => {
return {
name: 'yuxiaoliang',
id,
};
});
return {
user: data,
isLoading: !error && !data,
isError: error,
};
}
export default useUser;
具体的 swr 的用法不是本文的重点,具体可以看文档,本文用一个例子来引出对于 swr 原理的理解:
const sleep = async (times: number) => {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, times);
});
};
const { data: data500 } = useSWR('/api/user', async () => {
await sleep(500);
return { a: '500 is ok' };
});
const { data: data100 } = useSWR('/api/user', async () => {
await sleep(100);
return { a: '100 is ok' };
});
上述的代码中输出的是 data100 和 data500 分别是什么?
答案是:
data100和data500都输出了{a:'500 is ok '}
原因也很简单,在swr默认的时间内(默认是 2000
毫秒),对于同一个 useSWR
的 key
,这里的 key
是 ‘/api/user’
会进行重复值清除, 只始终 2000
毫秒内第一个 key
的 fetcher
函数来进行缓存更新。
带着这个例子,我们来深入读读 swr 的源码
二、swr的源码
我们从 useSWR
的 API 入手,来读一读 swr 的源码。首先在 swr 中本质是一种内存中的缓存更新策略,所以在 cache.ts
文件中,保存了缓存的 map
。
(1)cache.ts 缓存
class Cache implements CacheInterface {
constructor(initialData: any = {}) {
this.__cache = new Map(Object.entries(initialData))
this.__listeners = []
}
get(key: keyInterface): any {
const [_key] = this.serializeKey(key)
return this.__cache.get(_key)
}
set(key: keyInterface, value: any): any {
const [_key] = this.serializeKey(key)
this.__cache.set(_key, value)
this.notify()
}
keys() {
}
has(key: keyInterface) {
}
clear() {
}
delete(key: keyInterface) {
}
serializeKey(key: keyInterface): [string, any, 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 {
// convert null to ''
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
return [key, args, errorKey]
}
subscribe(listener: cacheListener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
let isSubscribed = true
this.__listeners.push(listener)
return () => {
//unsubscribe
}
}
// Notify Cache subscribers about a change in the cache
private notify() {
}
上述是 cache
类的定义,本质其实很简单,维护了一个 map
对象,以 key
为索引,其中key
可以是字符串,函数或者数组,将 key
序列化的方法为:serializeKey
serializeKey(key: keyInterface): [string, any, 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 {
// convert null to ''
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
return [key, args, errorKey]
}
从上述方法的定义中我们可以看出:
如果传入的
key
是字符串,那么这个字符串就是序列化后的key
如果传入的
key
是函数,那么执行这个函数,返回的结果就是序列化后的key
如果传入的
key
是数组,那么通过hash
方法(类似hash
算法,数组的值序列化后唯一)序列化后的值就是key
。
此外,在 cache
类中,将这个保存了 key
和 value
信息的缓存对象 map
,保存在实例对象 this.__cache
中,这个 this.__cache
对象就是一个 map
,有set get等方法。
(2)事件处理
在swr中,可以配置各种事件,当事件被触发时,会触发相应的重新请求或者说更新函数。swr对于这些事件,比如断网重连,切换 tab
重新聚焦某个 tab
等等,默认是会自动去更新缓存的。
在swr中对事件处理的代码为:
const revalidate = revalidators => {
if (!isDocumentVisible() || !isOnline()) return
for (const key in revalidators) {
if (revalidators[key][0]) revalidators[key][0]()
}
}
// focus revalidate
window.addEventListener(
'visibilitychange',
() => revalidate(FOCUS_REVALIDATORS),
false
)
window.addEventListener('focus', () => revalidate(FOCUS_REVALIDATORS), false)
// reconnect revalidate
window.addEventListener(
'online',
() => revalidate(RECONNECT_REVALIDATORS),
false
)
上述 FOCUS_REVALIDATORS
, RECONNECT_REVALIDATORS
事件中保存了相应的更新缓存函数,当页面触发事件visibilitychange(显示隐藏)、focus(页面聚焦)以及online(断网重连)的时候会触发事件,自动更新缓存 。
(3)useSWR 缓存更新的主体函数
useSWR
是swr的主体函数,决定了如何缓存以及如何更新,我们先来看 useSWR
的入参和形参。
入参:
key
: 一个唯一值,可以是字符串、函数或者数组,用来在缓存中唯一标识key
fetcher
: (可选) 返回数据的函数options
: (可选)对于useSWR
的一些配置项,比如事件是否自动触发缓存更新等等。
出参:
data
: 与入参key
相对应的,缓存中相应key
的value
值error
: 在请求过程中产生的错误等isValidating
: 是否正在请求或者正在更新缓存中,可以做为isLoading
等标识用。mutate(data?, shouldRevalidate?)
: 更新函数,手动去更新相应key
的value
值
从入参到出参,我们本质在做的事情,就是去控制 cache
实例,这个 map
的更新的关键是:
什么时候需要直接从缓存中取值,什么时候需要重新请求,更新缓存中的值。
const stateRef = useRef({
data: initialData,
error: initialError,
isValidating: false
})
const CONCURRENT_PROMISES = {} //以key为键,value为新的通过fetch等函数返回的值
const CONCURRENT_PROMISES_TS = {} //以key为键,value为开始通过执行函数获取新值的时间戳
下面我们来看,缓存更新的核心函数:revalidate
// start a revalidation
const revalidate = useCallback(
async (
revalidateOpts= {}
) => {
if (!key || !fn) return false
revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts)
let loading = true
let shouldDeduping =
typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
// start fetching
try {
dispatch({
isValidating: true
})
let newData
let startAt
if (shouldDeduping) {
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
if (fnArgs !== null) {
CONCURRENT_PROMISES[key] = fn(...fnArgs)
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
CONCURRENT_PROMISES_TS[key] = startAt = Date.now()
newData = await CONCURRENT_PROMISES[key]
setTimeout(() => {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}, config.dedupingInterval)
}
const shouldIgnoreRequest =
CONCURRENT_PROMISES_TS[key] > startAt ||
(MUTATION_TS[key] &&
(startAt <= MUTATION_TS[key] ||
startAt <= MUTATION_END_TS[key] ||
MUTATION_END_TS[key] === 0))
if (shouldIgnoreRequest) {
dispatch({ isValidating: false })
return false
}
cache.set(key, newData)
cache.set(keyErr, undefined)
// new state for the reducer
const newState: actionType<Data, Error> = {
isValidating: false
}
if (typeof stateRef.current.error !== 'undefined') {
// we don't have an error
newState.error = undefined
}
if (!config.compare(stateRef.current.data, newData)) {
// deep compare to avoid extra re-render
// data changed
newState.data = newData
}
// merge the new state
dispatch(newState)
if (!shouldDeduping) {
// also update other hooks
broadcastState(key, newData, undefined)
}
} catch (err) {
// catch err
}
loading = false
return true
},
[key]
)
上述代码已经通过简化, dispatch
就是更新 useSWR
返回值的函数:
const stateDependencies = useRef({
data: false,
error: false,
isValidating: false
})
const stateRef = useRef({
data: initialData,
error: initialError,
isValidating: false
})
let dispatch = useCallback(payload => {
let shouldUpdateState = false
for (let k in payload) {
stateRef.current[k] = payload[k]
if (stateDependencies.current[k]) {
shouldUpdateState = true
}
}
if (shouldUpdateState || config.suspense) {
if (unmountedRef.current) return
rerender({})
}
}, [])
在上述的 dispath
函数中,我们根据需要去更新 stateRef
, stateRef
的返回值,就是最终 useSWR
的返回值,这里的 rerender
是一个react hooks中的强制更新的一个hook:
const rerender = useState(null)[1]
每次执行 rerender({})
的时候,就会触发所在 hook
函数内组件的整体更新。其次我们还要再一次明确:
const CONCURRENT_PROMISES = {} //以key为键,value为新的通过fetch等函数返回的值
const CONCURRENT_PROMISES_TS = {} //以key为键,value为开始通过执行函数获取新值的时间戳
接着来看 revalidate
更新函数的核心部分:
let shouldDeduping =
typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
let newData
let startAt
if (shouldDeduping) {
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
if (fnArgs !== null) {
CONCURRENT_PROMISES[key] = fn(...fnArgs)
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
CONCURRENT_PROMISES_TS[key] = startAt = Date.now()
newData = await CONCURRENT_PROMISES[key]
setTimeout(() => {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}, config.dedupingInterval)
}
上述代码中, shouldDeduping
是用来判断是否需要去重的依据,从上述代码可以看出 config.dedupingInterval
的默认值是 2000
毫秒,也就是在 2000
毫秒内,对于同一个 key
会去重,也就是说,如果 2000
毫秒内,对于同一个 key
,同时发起了多个更新函数,那么会以第一次更新的结果为准。以 key
为键,记录每个 key
发起的时候的时间戳的数组是 CONCURRENT_PROMISES_TS
,而 CONCURRENT_PROMISES
,由此可以看出,更准确 的说法是:
一定时间内,去重后的key和value的值的集合,key是useSWR中的唯一key,也就是cache实例map的key,value就是最新的缓存中更新过的值。
(4)useSWR 中如何更新
根据上述的代码我们知道了更新函数是怎么样的,在内存中保存了 CONCURRENT_PROMISES_TS
这个对象,其 key
为 cache
中的 key
, value
为最新的值,那么如何在 CONCURRENT_PROMISES_TS
对象 key
所对应的值发生变化的时候,去更新 useSWR
实例的返回值,从而达到我们最终的缓存更新效果呢。
我们接着来看代码:
//保存对象
const CACHE_REVALIDATORS = {}
//具体更新函数
const onUpdate: updaterInterface<Data, Error> = (
shouldRevalidate = true,
updatedData,
updatedError,
dedupe = true
) => {
// update hook state
const newState: actionType<Data, Error> = {}
let needUpdate = false
if (
typeof updatedData !== 'undefined' &&
!config.compare(stateRef.current.data, updatedData)
) {
newState.data = updatedData
needUpdate = true
}
if (stateRef.current.error !== updatedError) {
newState.error = updatedError
needUpdate = true
}
//更新当前的stateRef
if (needUpdate) {
dispatch(newState)
}
if (shouldRevalidate) {
return revalidate()
}
return false
}
//增加监听key
const addRevalidator = (revalidators, callback) => {
if (!callback) return
if (!revalidators[key]) {
revalidators[key] = [callback]
} else {
revalidators[key].push(callback)
}
}
addRevalidator(CACHE_REVALIDATORS, onUpdate)
//更新缓存的方法
const broadcastState: broadcastStateInterface = (key, data, error) => {
const updaters = CACHE_REVALIDATORS[key]
if (key && updaters) {
for (let i = 0; i < updaters.length; ++i) {
updaters[i](false, data, error)
}
}
}
broadcastState
方法会在每一次更新 cache
的 key
的时候触发,而 CACHE_REVALIDATORS
保存了所有与 key
相关的更新函数,这里需要注意的是:
为什么CACHE_REVALIDATORS[key]的值是一个数组?
因为 useSWR
的 key
,同一个 key
可以有多个更新函数,因此 CACHE_REVALIDATORS[key]
是一个数组。
举例来说,在同一个组件中使用两个同名 key
,但是他们的更新函数不同,是被允许的:
const { data: data500 } = useSWR('/api/user', async () => {
await sleep(500);
return { message: '500 is ok' };
});
const { data: data100 } = useSWR('/api/user', async () => {
await sleep(100);
return { message: '100 is ok' };
});
(5)mutate 主动触发更新函数
了解了useSWR
中的更新,那么剩下的这个 mutate
就及其简单:
const mutate: mutateInterface = async ()=>{
let data, error
if (_data && typeof _data === 'function') {
// `_data` is a function, call it passing current cache value
try {
data = await _data(cache.get(key))
} catch (err) {
error = err
}
} else if (_data && typeof _data.then === 'function') {
// `_data` is a promise
try {
data = await _data
} catch (err) {
error = err
}
} else {
data = _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, i > 0))
}
// return new updated value
return Promise.all(promises).then(() => {
if (error) throw error
return cache.get(key)
})
}
}
简单的说就是拿到值,然后调用 const updaters = CACHE_REVALIDATORS[key]
数组中的每一个更新函数,更新相应的 useSWR
的值即可。这里 data
的值可以是直接从缓存中取,或者是手动传入(类似于乐观更新的方式)。
最后
欢迎关注「前端瓶子君」,回复「交流」加入前端交流群!
欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!
在这里(算法群),你可以每天学习一道大厂算法编程题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!
另外,每周还有手写源码题,瓶子君也会解答哟!
》》面试官也在看的算法资料《《
“在看和转发”就是最大的支持