受權轉載自:JonyYu 前端
https://github.com/forthealllight/blog/issues/61react
swr是一個hook組件,能夠做爲請求庫和狀態管理庫,本文主要介紹一下在項目中如何實戰使用swr,而且會解析一下swr的原理。從原理出發讀一讀swr的源碼ios
什麼是swr swr的的源碼
1、什麼是swr
useSWR
是 react hooks 中一個比較有意思的組件,既能夠做爲請求庫,也能夠做爲狀態管理的緩存用,SWR 的名字來源於「stale-while-revalidate」, 是在HTTP RFC 5861標準中提出的一種緩存更新策略 :git
首先從緩存中取數據,而後去真實請求相應的數據,最後將緩存值和最新值作對比,若是緩存值與最新值相同,則不用更新,不然用最新值來更新緩存,同時更新UI展現效果。github
useSWR
能夠做爲請求庫來用:web
//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 分別是什麼?編程
答案是:json
data100和data500都輸出了{a:'500 is ok '}
緣由也很簡單,在swr默認的時間內(默認是 2000
毫秒),對於同一個 useSWR
的 key
,這裏的 key
是 ‘/api/user’
會進行重複值清除, 只始終 2000
毫秒內第一個 key
的 fetcher
函數來進行緩存更新。
帶着這個例子,咱們來深刻讀讀 swr 的源碼
2、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
的值能夠是直接從緩存中取,或者是手動傳入(相似於樂觀更新的方式)。
最後

本文分享自微信公衆號 - 前端瓶子君(pinzi_com)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。