Open forthealllight opened 4 years ago
swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目中如何实战使用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的源码
,我们从useSWR的API入手,来读一读swr的源码。首先在swr中本质是一种内存中的缓存更新策略,所以在cache.ts文件中,保存了缓存的map。
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] }
从上述方法的定义中我们可以看出:
此外,在cache类中,将这个保存了key和value信息的缓存对象map,保存在实例对象this.cache中,这个this.cache对象就是一个map,有set get等方法。
在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(断网重连)的时候会触发事件,自动更新缓存。
useSWR是swr的主体函数,决定了如何缓存以及如何更新,我们先来看useSWR的入参和形参。
入参:
出参:
从入参到出参,我们本质在做的事情,就是去控制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就是最新的缓存中更新过的值。
根据上述的代码我们知道了更新函数是怎么样的,在内存中保存了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' }; });
了解了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的值可以是直接从缓存中取,或者是手动传入(类似于乐观更新的方式)。
React hooks中swr的原理和源码解析
swr是一个hook组件,可以作为请求库和状态管理库,本文主要介绍一下在项目中如何实战使用swr,并且会解析一下swr的原理。从原理出发读一读swr的源码
一、什么是swr
useSWR是react hooks中一个比较有意思的组件,既可以作为请求库,也可以作为状态管理的缓存用,SWR的名字来源于“stale-while-revalidate”, 是在HTTP RFC 5861标准中提出的一种缓存更新策略 :
首先从缓存中取数据,然后去真实请求相应的数据,最后将缓存值和最新值做对比,如果缓存值与最新值相同,则不用更新,否则用最新值来更新缓存,同时更新UI展示效果。
useSWR可以作为请求库来用:
此外,因为相同的key总是返回相同的实例,在useSWR中只保存了一个cache实例,因此useSWR也可以当作全局的状态管理机。比如可以全局保存用户名称 :
具体的swr的用法不是本文的重点,具体可以看文档,本文用一个例子来引出对于swr原理的理解:
上述的代码中输出的是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 缓存
上述是cache类的定义,本质其实很简单,维护了一个map对象,以key为索引,其中key可以是字符串,函数或者数组,将key序列化的方法为:serializeKey
从上述方法的定义中我们可以看出:
此外,在cache类中,将这个保存了key和value信息的缓存对象map,保存在实例对象this.cache中,这个this.cache对象就是一个map,有set get等方法。
(2)事件处理
在swr中,可以配置各种事件,当事件被触发时,会触发相应的重新请求或者说更新函数。swr对于这些事件,比如断网重连,切换tab重新聚焦某个tab等等,默认是会自动去更新缓存的。
在swr中对事件处理的代码为:
上述FOCUS_REVALIDATORS,RECONNECT_REVALIDATORS事件中保存了相应的更新缓存函数,当页面触发事件visibilitychange(显示隐藏)、focus(页面聚焦)以及online(断网重连)的时候会触发事件,自动更新缓存。
(3)useSWR 缓存更新的主体函数
useSWR是swr的主体函数,决定了如何缓存以及如何更新,我们先来看useSWR的入参和形参。
入参:
出参:
从入参到出参,我们本质在做的事情,就是去控制cache实例,这个map的更新的关键是:
什么时候需要直接从缓存中取值,什么时候需要重新请求,更新缓存中的值。
下面我们来看,缓存更新的核心函数:revalidate
上述代码已经通过简化,dispatch就是更新useSWR返回值的函数:
在上述的dispath函数中,我们根据需要去更新stateRef,stateRef的返回值,就是最终useSWR的返回值,这里的rerender是一个react hooks中的强制更新的一个hook:
每次执行rerender({})的时候,就会触发所在hook函数内组件的整体更新。其次我们还要再一次明确:
接着来看revalidate更新函数的核心部分:
上述代码中,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实例的返回值,从而达到我们最终的缓存更新效果呢。
我们接着来看代码:
broadcastState方法会在每一次更新cache的key的时候触发,而CACHE_REVALIDATORS保存了所有与key相关的更新函数,这里需要注意的是:
为什么CACHE_REVALIDATORS[key]的值是一个数组?
因为useSWR的key,同一个key可以有多个更新函数,因此CACHE_REVALIDATORS[key]是一个数组。
举例来说,在同一个组件中使用两个同名key,但是他们的更新函数不同,是被允许的:
(5)mutate 主动触发更新函数
了解了useSWR中的更新,那么剩下的这个mutate就及其简单:
简单的说就是拿到值,然后调用const updaters = CACHE_REVALIDATORS[key]数组中的每一个更新函数,更新相应的useSWR的值即可。这里data的值可以是直接从缓存中取,或者是手动传入(类似于乐观更新的方式)。