文章來源于公眾號:前端瓶子君
swr是一個hook組件,可以作為請求庫和狀態(tài)管理庫,本文主要介紹一下在項(xiàng)目中如何實(shí)戰(zhàn)使用swr,并且會解析一下swr的原理。從原理出發(fā)讀一讀swr的源碼
- 什么是swr
- swr的的源碼
一、什么是swr
useSWR
是 react hooks 中一個比較有意思的組件,既可以作為請求庫,也可以作為狀態(tài)管理的緩存用,SWR 的名字來源于“stale-while-revalidate”, 是在HTTP RFC 5861標(biāo)準(zhǔn)中提出的一種緩存更新策略 :
首先從緩存中取數(shù)據(jù),然后去真實(shí)請求相應(yīng)的數(shù)據(jù),最后將緩存值和最新值做對比,如果緩存值與最新值相同,則不用更新,否則用最新值來更新緩存,同時(shí)更新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
)
// ...
}
此外,因?yàn)橄嗤?key
總是返回相同的實(shí)例,在 useSWR
中只保存了一個 cache
實(shí)例,因此 useSWR
也可以當(dāng)作全局的狀態(tài)管理機(jī)。比如可以全局保存用戶名稱 :
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 的用法不是本文的重點(diǎn),具體可以看文檔,本文用一個例子來引出對于 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都輸出了{(lán)a:'500 is ok '}
原因也很簡單,在swr默認(rèn)的時(shí)間內(nèi)(默認(rèn)是 2000
毫秒),對于同一個 useSWR
的 key
,這里的 key
是 ‘/api/user’
會進(jìn)行重復(fù)值清除, 只始終 2000
毫秒內(nèi)第一個 key
的fetcher
函數(shù)來進(jìn)行緩存更新。
帶著這個例子,我們來深入讀讀 swr 的源碼
二、swr的源碼
我們從 useSWR
的 API 入手,來讀一讀 swr 的源碼。首先在 swr 中本質(zhì)是一種內(nèi)存中的緩存更新策略,所以在 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
類的定義,本質(zhì)其實(shí)很簡單,維護(hù)了一個 map
對象,以 key
為索引,其中key
可以是字符串,函數(shù)或者數(shù)組,將 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
是函數(shù),那么執(zhí)行這個函數(shù),返回的結(jié)果就是序列化后的key
- 如果傳入的
key
是數(shù)組,那么通過hash
方法(類似hash
算法,數(shù)組的值序列化后唯一)序列化后的值就是key
。
此外,在 cache
類中,將這個保存了 key
和 value
信息的緩存對象 map
,保存在實(shí)例對象 this.__cache
中,這個 this.__cache
對象就是一個 map
,有set get等方法。
(2)事件處理
在swr中,可以配置各種事件,當(dāng)事件被觸發(fā)時(shí),會觸發(fā)相應(yīng)的重新請求或者說更新函數(shù)。swr對于這些事件,比如斷網(wǎng)重連,切換 tab
重新聚焦某個 tab
等等,默認(rèn)是會自動去更新緩存的。
在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
事件中保存了相應(yīng)的更新緩存函數(shù),當(dāng)頁面觸發(fā)事件visibilitychange(顯示隱藏)、focus(頁面聚焦)以及online(斷網(wǎng)重連)的時(shí)候會觸發(fā)事件,自動更新緩存 。
(3)useSWR 緩存更新的主體函數(shù)
useSWR
是swr的主體函數(shù),決定了如何緩存以及如何更新,我們先來看 useSWR
的入?yún)⒑托螀ⅰ?/p>
入?yún)?
key
: 一個唯一值,可以是字符串、函數(shù)或者數(shù)組,用來在緩存中唯一標(biāo)識key
fetcher
: (可選) 返回?cái)?shù)據(jù)的函數(shù)options
: (可選)對于useSWR
的一些配置項(xiàng),比如事件是否自動觸發(fā)緩存更新等等。
出參:
data
: 與入?yún)?key
相對應(yīng)的,緩存中相應(yīng)key
的value
值error
: 在請求過程中產(chǎn)生的錯誤等isValidating
: 是否正在請求或者正在更新緩存中,可以做為isLoading
等標(biāo)識用。mutate(data?, shouldRevalidate?)
: 更新函數(shù),手動去更新相應(yīng)key
的value
值
從入?yún)⒌匠鰠?,我們本質(zhì)在做的事情,就是去控制 cache
實(shí)例,這個 map
的更新的關(guān)鍵是:
什么時(shí)候需要直接從緩存中取值,什么時(shí)候需要重新請求,更新緩存中的值。
const stateRef = useRef({
data: initialData,
error: initialError,
isValidating: false
})
const CONCURRENT_PROMISES = {} //以key為鍵,value為新的通過fetch等函數(shù)返回的值
const CONCURRENT_PROMISES_TS = {} //以key為鍵,value為開始通過執(zhí)行函數(shù)獲取新值的時(shí)間戳
下面我們來看,緩存更新的核心函數(shù):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