# 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)