# learning-swr
**Repository Path**: perdream/learning-swr
## Basic Information
- **Project Name**: learning-swr
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2023-12-24
- **Last Updated**: 2023-12-27
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
#
认识useSWR: 用于数据请求的 React Hooks 库
## 简介[1](#refer-1)
“**SWR**” 这个名字来自于 `stale-while-revalidate`:一种由 [HTTP RFC 5861](https://tools.ietf.org/html/rfc5861) 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。
## 使用背景
日常React开发中,数据请求是一个很普遍的功能,保存接口返回的数据,请求状态以及错误。
编写页面请求数据的代码可能是类似下面这样:
```js
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const data = await fetch('http://xxx')
setData(data)
}catch(err) {
setError(err)
}finally {
setLoading(false)
}
}
fetchData()
}, [])
```
为了方便在多个组件间使用,会封装成一个hook, 类似下面这样:
```js
export function useFetch(url) {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const data = await fetch('http://xxx')
setState(data)
}catch(err) {
setError(err)
}finally {
setLoading(false)
}
}
fetchData()
}, [url])
return [data, loading, error]
}
```
如果需要支持使用自定义的fetch方法请求数据,代码可能就类似这样:
```js
export function useFetch(url, fetch) {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const data = await fetch('http://xxx')
setState(data)
}catch(err) {
setError(err)
}finally {
setLoading(false)
}
}
fetchData()
}, [url])
return [data, loading, error]
}
```
这时要考虑如果用户在每个组件使用useFetch这个hook都要传递这个自定义的请求方法就显得很麻烦,
就容易想到做一个全局的配置,使用useContext,改造后代码类似这样:
```js
// 导出FetchConfigContext
FetchConfigContext = React.createContext({})
export default FetchConfigContext
// 创建一个文件导出FetchConfigContext.provider
import axios from 'axios'
const defaultConfig = {
fetcher: axios
}
Object.defineProperty(FetchConfigContext.Provider, 'default', {
value: defaultConfig
})
export default FetchConfigContext.Provider
// 使用
import axios from 'axios'
import {SWRConfig} from 'XXX'
function App() {
return (
);
}
// 自定义的 useFetch
import FetchConfigContext from 'FetchConfigContext'
export function useFetch(url, fetcher) {
const [data, setData] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const config = useContext(FetchConfigContext)
const fn = fetcher || config.fetcher
useEffect(() => {
const fetchData = async () => {
setLoading(true)
try {
const data = await fn('http://xxx')
setState(data)
}catch(err) {
setError(err)
}finally {
setLoading(false)
}
}
fetchData()
}, [url])
return [data, loading, error]
}
```
通过上面的修改,实现了一个基本的数据请求,但是还存在一些问题,比如:
- 在请求地址相同的情况下,短时间内连续调用多次useFetch请求都会发出
- 发生错误时,可否自动重试
- 当请求间具有依赖关系,比如前一次的请求结果用户后一次的请求参数,如何保证请求的有序
- 等等。。。
由此产生[SWR](https://swr.vercel.app/), 这是Next.js团队创建的一个React请求库, 支持以下一些特性
- 数据请求
- 全局请求配置
- 有条件的发出请求
- 请求间有依赖,保证请求顺序
- 接口数据缓存
- 手动数据校验
- 错误重试
- 默认支持请求去重
- 定时轮训更新缓存
- 等等。。。
## 基本使用
### 数据请求
```js
import useSWR from 'swr'
function Profile() {
const { data, error, isValidating, mutate } = useSWR('/api/user', fetcher, options)
if (error) return failed to load
if (!data) return loading...
return hello {data.name}!
}
```
#### 请求参数
在这个例子中,useSWR接受了三个参数分别是 请求的地址、请求方法、useSWR的参数配置,在useSWR中,第一个参数被称为key, 这个key被作为请求的唯一标识,也是基于这个key做的接口数据缓存。第二个参数称为fetcher,顾名思义是请求的方法,接收第一个参数作为它的参数异步的返回数据,第三个是一个对象[options](#opions-参数),用于配置useSWR这个hook
#### opions 参数
```js
// 错误重试间隔
errorRetryInterval: (slowConnection ? 10 : 5) * 1000,
// 页面可见时请求节流间隔
focusThrottleInterval: 5 * 1000,
// 重复数据存在的间隔,
dedupingInterval: 2 * 1000,
// 请求超时时间
loadingTimeout: (slowConnection ? 5 : 3) * 1000,
// 刷新数据间隔,0代表不刷新
refreshInterval: 0,
// 页面可见时是否需要重新请求
revalidateOnFocus: true,
// 浏览器网络重新连接时是否需要重新请求
revalidateOnReconnect: true,
// 页面不可见时,是否需要刷新
refreshWhenHidden: false,
// 浏览器无网络时,是否需要刷新
refreshWhenOffline: false,
// 发生错误后是否进行重试
shouldRetryOnError: true,
// 是否是Concurrent模式
suspense: false,
// 比较data值函数,默认是深比较
compare: deepEqual
```
#### 返回值
- data 返回的数据
- error fetcher中抛出的错误
- isValidating 类似于loading,
- mutate 手动更新缓存数据的方法
### 全局请求配置
```js
import useSWR, { SWRConfig } from 'swr'
function Dashboard() {
const { data: events } = useSWR('/api/events')
const { data: projects } = useSWR('/api/projects')
const { data: user } = useSWR('/api/user', { refreshInterval: 0 }) // 不会定时更新
}
function App() {
return (
fetch(...args).then(res => res.json())
}}
>
)
}
```
### 有条件的发出请求
当提供的key为null 或者 为falsy值的时候,就不会发出请求
```js
// shouldFetch 为true的时候才会发出请求
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// 第一个参数可以是一个函数,函数的返回值为falsy或者报错都不会发出请求
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)
```
### 请求间有依赖
当后一个请求的结果需要依赖于前一个请求的返回值时
```js
function MyProjects() {
const { data: user } = useSWR('/api/user')
/**原理在于/api/user 数据未返回时,user.id就会报错,此时/api/projects?uid=的url请求就不会被执行。
当user有值的时候,也就是第一个请求成功返回后,就会强制做rerender重新刷新渲染组件,这时候第二个请求就可以发送出去。
*/
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
```
### 接口数据缓存
相同的key去调用useSWR,在dedupingInterval(缓存中存在数据的时间间隔)内,
只会发送第一个请求,其他请求会取消。
### 错误重试
可以自定义onErrorRetry方法
```js
useSWR(key, fetcher, {
onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
if (retryCount >= 10) return
if (error.status === 404) return
// retry after 5 seconds
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
}
})
```
### useSWR 核心流程图[2](#refer-2)
### 核心代码解析(基于0.5.4 [github](https://github.com/super-wall/swr/tree/peel/src))
#### 全局配置
`swr-config-context.ts` 导出ReactContext
```js
const SWRConfigContext = createContext({})
SWRConfigContext.displayName = 'SWRConfigContext'
export default SWRConfigContext
```
`use-swr.ts` 中合并用户自定义配置和默认配置
```js
// 合并配置,优先级:hook调用传入的config > 全局配置 > 默认配置
config = Object.assign(
{},
defaultConfig,
useContext(SWRConfigContext),
config
)
// configRef始终是最新的配置
const configRef = useRef(config)
useIsomorphicLayoutEffect(() => {
configRef.current = config
})
```
#### 有条件的发出请求
useSWR的有条件发出请求,是根据key是否为falsy从而终止请求
第一个参数key可以传字符串,函数,或者数组,在useSWR中将会
处理成字符串并对key值做判断请求是否继续往下执行
`cache.ts`
```js
serializeKey(key: Key): [string, any, string, string] {
let args = null
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// key 为空后续fetcher就不会调用
key = ''
}
}
if (Array.isArray(key)) {
args = key
key = hash(key)
} else {
key = String(key || '')
}
const errorKey = key ? 'err@' + key : ''
const isValidatingKey = key ? 'validating@' + key : ''
return [key, args, errorKey, isValidatingKey]
}
```
`use-swr.ts`
```js
useIsomorphicLayoutEffect(() => {
// key 为falsy则不处理
if(!key) return
// 省略代码...
})
```
#### 接口数据缓存
useSWR采用一个WeakMap做全局的数据缓存
`cache.ts`
```js
export default class Cache implements CacheType {
private cache: Map
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()
}
// 省略代码...
}
```
#### 发送请求获取数据,并做请求去重
根据配置的fetcher发送请求获取数据
`use-swr.ts`
```js
// 重新取数,返回布尔值Promise
const revalidate = useCallback(
async ( revalidateOpts: RevalidateOptionInterface = {} ): Promise => {
// 请求标识符或请求函数不存在直接返回false
if (!key || !fn) return false
// 组件已卸载返回false
if (unmountedRef.current) return false
revalidateOpts = Object.assign({ dedupe: false }, revalidateOpts)
// loading状态
let loading = true
// 是否可以使用重复请求。相同的请求未过期时(config.dedupingInterval间隔会清除一次),并且开启了去重
let shouldDeduping = typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe
// 开始异步请求
try {
dispatch({ isValidating: true })
let newData
let startAt
// 已经有一个正在进行的请求,需要去重,直接使用之前的就可以。
if (shouldDeduping) {
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
// 如果缓存为空,且请求超时,则触发onLoadingSlow回调
if (config.loadingTimeout && !cache.get(key)) {
setTimeout(() => {
if (loading) eventsRef.current.emit('onLoadingSlow', key, config)
}, config.loadingTimeout)
}
// useSWR传入数组,fnArgs是该数组,当做参数执行请求函数(默认首个参数是请求url, 其他是fn需要的参数)
if (fnArgs !== null) {
CONCURRENT_PROMISES[key] = fn(...fnArgs)
} else {
CONCURRENT_PROMISES[key] = fn(key)
}
// 此次请求的时间戳
CONCURRENT_PROMISES_TS[key] = startAt = Date.now()
// 将请求结果赋值给newData
newData = await CONCURRENT_PROMISES[key]
/** dedupingInterval时间后,从对象上删除此次请求,
这段时间内,如果开启了dedupe,多次相同key的请求都只会复用
*/
setTimeout(() => {
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
}, config.dedupingInterval)
// 触发成功事件
eventsRef.current.emit('onSuccess', newData, key, config)
}
const shouldIgnoreRequest =
// 如果有其他正在进行的请求发生在此请求之后,我们需要忽略当前请求,以后面的为准
CONCURRENT_PROMISES_TS[key] > startAt ||
// 如果有其他突变,要忽略当前请求,因为它不是最新的了。
// 同时 突变结束后,一个新的取数应该被触发
// case 1:
// req------------------>res
// mutate------>end
// case 2:
// req------------>res
// mutate------>end
// case 3:
// req------------------>res
// mutate-------...---------->
(MUTATION_TS[key] &&
// case 1
(startAt <= MUTATION_TS[key] ||
// case 2
startAt <= MUTATION_END_TS[key] ||
// case 3
MUTATION_END_TS[key] === 0))
if (shouldIgnoreRequest) {
dispatch({ isValidating: false })
return false
}
cache.set(key, newData)
cache.set(keyErr, undefined)
// 为dispatch函数创建新的state
const newState: actionType = {
isValidating: false
}
// 此次请求没有发生错误,如果之前是错误,需要修改
if (typeof stateRef.current.error !== 'undefined') {
newState.error = undefined
}
// 请求结果不相等时(深度比较),更新
if (!config.compare(stateRef.current.data, newData)) {
newState.data = newData
}
// 更新state,触发渲染。
dispatch(newState)
if (!shouldDeduping) {
// 同时更新其他钩子函数
broadcastState(key, newData, undefined)
}
} catch (err) {
// 捕获错误, 删除此次请求的promise
delete CONCURRENT_PROMISES[key]
delete CONCURRENT_PROMISES_TS[key]
// 缓存:设置错误
cache.set(keyErr, err)
// 发生错误不同,更新state
if (stateRef.current.error !== err) {
dispatch({
isValidating: false,
error: err
})
if (!shouldDeduping) {
// 同时更新其他钩子函数
broadcastState(key, undefined, err)
}
}
// 触发onError事件回调
eventsRef.current.emit('onError', err, key, config)
// 发生错误后是否进行重试
if (config.shouldRetryOnError) {
// 当重试时,需要启动清除重复,一直维护重试次数
const retryCount = (revalidateOpts.retryCount || 0) + 1
eventsRef.current.emit(
'onErrorRetry',
err,
key,
config,
revalidate,
Object.assign({ dedupe: true }, revalidateOpts, { retryCount })
)
}
}
loading = false
return true
},
[key]
)
```
#### 错误重试
用户配置了shouldRetryOnError: true(默认是true)则在获取数据出现错误时则调用
onErrorRetry 方法并返回 revalidate(请求数据的方式)等值供用户自定义错误重试
`use-swr.ts`
```js
// 触发onError事件回调
eventsRef.current.emit('onError', err, key, config)
// 发生错误后是否进行重试
if (config.shouldRetryOnError) {
// 当重试时,需要启动清除重复,一直维护重试次数
const retryCount = (revalidateOpts.retryCount || 0) + 1
eventsRef.current.emit(
'onErrorRetry',
err,
key,
config,
revalidate,
Object.assign({ dedupe: true }, revalidateOpts, { retryCount })
)
}
```
#### 轮询更新
如果用户配置了
- refreshInterval:true
- refreshWhenHidden:true
- refreshWhenOffline:true
则会开启定时轮询更新
`use-swr.ts`
```js
useIsomorphicLayoutEffect(() => {
let timer: any = null
const tick = async () => {
if (
!stateRef.current.error &&
(configRef.current.refreshWhenHidden ||
configRef.current.isDocumentVisible()) &&
(configRef.current.refreshWhenOffline || configRef.current.isOnline())
) {
await revalidate({ dedupe: true })
}
if (configRef.current.refreshInterval && timer) {
timer = setTimeout(tick, configRef.current.refreshInterval)
}
}
if (configRef.current.refreshInterval) {
timer = setTimeout(tick, configRef.current.refreshInterval)
}
return () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
}, [
config.refreshInterval,
config.refreshWhenHidden,
config.refreshWhenOffline,
revalidate
])
```
#### 断网重连、页面聚焦重新请求
useSWR的配置中默认打开revalidateOnFocus 、revalidateOnReconnect。
通过监听浏览器的visibilitychange、focus事件实现页面聚焦发起请求
通过监听浏览器online事件实现断开重连发起请求
`libs/web-preset.ts`
```js
const registerOnFocus = (cb: () => void) => {
if (
typeof window !== 'undefined' &&
window.addEventListener !== undefined &&
typeof document !== 'undefined' &&
document.addEventListener !== undefined
) {
document.addEventListener('visibilitychange', () => cb(), false)
window.addEventListener('focus', () => cb(), false)
}
}
const registerOnReconnect = (cb: () => void) => {
if (typeof window !== 'undefined' && window.addEventListener !== undefined) {
window.addEventListener(
'online',
() => {
online = true
cb()
},
false
)
window.addEventListener('offline', () => (online = false), false)
}
}
```
以上为一些主要的源码逻辑解析,源码中还有很多有特色的功能点,本文是基于0.5.4版本编写,目前已经更新到了2.2.4
感兴趣的可以查看[最新源码](https://github.com/vercel/swr), 总的来说useSWR是一个轻量用于数据请求的库,
使用useSWR可以使开发更加简洁高效。
## 参考
- [1] [useSWR官网](https://swr.bootcss.com/)
- [2] [深入SWR 设计与源码分析](https://juejin.cn/post/7078580725607497742)