SWR源码学习

SWR源码学习

SWR是一个基于React hook做的数据请求库。社区上对标的库React-Query。两个库的在这就不做对比了。因为没有研究过React-Query。最近新开了一个项目,主要还看看中SWR的出名度和SSR完美兼容。新的项目承载着公司的基础架构,所以我个人还是希望去看看它内部做了什么。从官网的文档来看是挺简单,但是它的缓存的规则就不了了。

Source: SWR

从官网上看,SWR是一个数据请求的库,它的特性也很简单

  • 轻量
  • 支持cache
  • 支持实时
  • 支持typescript

初始化

当加载SWR function的时候,SWR会进行初始化,初始化的过程是先从Cache里面取值,如果没有那么会进行相对应的初始化。

stateRef是在SWR内部里的一个状态,它里面存储着,data、error、isValidating. 在初始化的时候,data和error先会去cache里面去取值。cache是根据它的key做取值了。每一个useSWR中,它的第一个参数都为它的key。例如:

import React from 'react';
import useSWR from 'swr';

const UserName = () => {
const { data, error } = useSWR('/user'); // '/user', 就是useSWR内部里面的key
return !data ? (
<span>loading...</span>
) : (
<span>{data.name}</span>
);
}

在这里不禁要问,它的key怎么来,单纯的字符串并不满足文档上说的:

useSWR的第一个参数可以为string | function | 数组,数组可以传递给fetcher当作参数

在源码中,cache文件里的serializeKey function就是做这样的一件事的。它的参数是key,返回值为一个数组: [key, args, errorKey].

// TODO: introduce namespace for the cache
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
// 作者在hash中使用了weakmap
// use WeakMap to store the object->key mapping
// so the objects can be garbage collected.
// WeakMap uses a hashtable under the hood, so the lookup
// complexity is almost O(1).
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}

const errorKey = key ? 'err@' + key : ''

return [key, args, errorKey]
}

下面来看下stateRef初始化的过程:

const initialData = cache.get(key) || config.initialData  // config来源于useSWR的第二个参数以及它内部默认的值
const initialError = cache.get(keyErr)

// stateDependencies的定义是为了方便数据变动的时候执行rerender操作
const stateDependencies = useRef({
data: false,
error: false,
isValidating: false
})
// 这里的stateRef就是它主要返回的API
const stateRef = useRef({
data: initialData,
error: initialError,
isValidating: false
})
// 这里非常巧妙。用了useState模拟了一个rerender函数。因为每次触发setState的时候都会进行rerender
const rerender = useState(null)[1]

SWR过程关联函数

介绍SWR内部过程机制之前,先要看看与过程相关的函数。

useIsomorphicLayoutEffect

这个函数其实就是useEffect或者useLayoutEffect。一个在服务端使用,一个在浏览器中使用

// React currently throws a warning when using useLayoutEffect on the server.
// To get around it, we can conditionally useEffect on the server (no-op) and
// useLayoutEffect in the browser.
const useIsomorphicLayoutEffect = IS_SERVER ? useEffect : useLayoutEffect

dispatch

dispatch是一个触发器,看看具体源码.里面有个细节就是用let定义的,而不是const。根据下面的代码,是为了组件挂载之后直接设为null。这块具体用意还不清楚。

let dispatch = useCallback(payload => {
let shouldUpdateState = false
for (let k in payload) {
stateRef.current[k] = payload[k]
if (stateDependencies.current[k]) {
shouldUpdateState = true
}
}
// 从文档看,swr不建议现在使用suspense,因为会改动
if (shouldUpdateState || config.suspense) {
if (unmountedRef.current) return
rerender({})
}
}, [])

这个方法很直观。在初始化那我介绍过stateDependencies其实是一个判断是否更新的对象。所以dispatch内部先去查传递过来的payload里面有没有需要更新的,如果有那么就执行rerender。

revalidate

这个function可以说是SWR的核心。里面不仅仅处理数据校验的过程,还有请求数据,更新数据等操作。

在revalidate中,首先先判断当前的渲染是不是有重复的数据,如果是,那么等待它处理完成。这里有两个变量去存储正在处理的请求

let shouldDeduping =
typeof CONCURRENT_PROMISES[key] !== 'undefined' && revalidateOpts.dedupe

if (shouldDeduping) {
// there's already an ongoing request,
// this one needs to be deduplicated.
startAt = CONCURRENT_PROMISES_TS[key]
newData = await CONCURRENT_PROMISES[key]
} else {
// if no cache being rendered currently (it shows a blank page),
// we trigger the loading slow event.
if (config.loadingTimeout && !cache.get(key)) {
setTimeout(() => {
if (loading) eventsRef.current.emit('onLoadingSlow', key, config)
}, config.loadingTimeout)
}

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)

// trigger the success event,
// only do this for the original request.
eventsRef.current.emit('onSuccess', newData, key, config)
}

这段代码首先去判断是否属于重复数据,如果是,那么CONCURRENT_PROMISES里面就有正在执行的promise,直接去里面取数据就可以了。

如果不是,那么先触发一个onLoadingSlow的事件,这个事件是一个根据loadingTimeout的setTimeout,意味着当请求时长过长的时候,这个事件就会被触发(这个是需求确实是很精致)。

接着就是将请求放入CONCURRENT_PROMISES变量里面,然后设置当前的开始时间。然后开始请求数据。

请求完数据之后根据config里面设置的dedupingInterval,删除请求。举个例子,目前官方的时间为2000ms,也就是说2秒内遇到重复请求数据的时候,只执行第一次。当然,如果你设置成0,那么就是每次都会触发请求。

最后,触发一次onSuccess的方法。此时,SWR已经拿到Backend的Data了,然后把key和对应的data存到cache里面。此时已经完成了revalidate的流程。但是还有一个特别重要的概念:Mutation。之后在Mutation我会讲如何实现。

上面已经讲完了useIsomorphicLayoutEffect相关的函数,那么接下来我将要去看下useIsomorphicLayoutEffect里面到底干了什么事情,让SWR那么高效。

过程

在SWR里面有使用了两次useIsomorphicLayoutEffect,依赖都是不一样的

// 第一次
useIsomorphicLayoutEffect(() => {
// code...
}, [key, revalidate]);

// 第二次
useIsomorphicLayoutEffect(() => {
// code...
}, [
// 这三个参数是处理当页面处于休眠状态,比如
config.refreshInterval, // 这个应该就是实时更新了。默认为0,也就是disabled
config.refreshWhenHidden, // 当页面处于休眠状态的时候,如切换浏览器的tab,浏览器最小化等
config.refreshWhenOffline, // polling when the browser is offline (determined by navigator.onLine)
revalidate
]);

过程里面我列出下面几个关键字:校验、闲时、cache。

校验

当执行useSWR的时候,先会执行数据校验。但是执行revalidate是利用浏览器requestIdleCallback来触发这个revalidate,这样在浏览器闲时的时候才去进行数据请求。我想这也是它为什么快的原因。

// revalidate with deduping
const softRevalidate = () => revalidate({ dedupe: true })

// trigger a revalidation
if (
config.revalidateOnMount ||
(!config.initialData && config.revalidateOnMount === undefined)
) {
if (
typeof latestKeyedData !== 'undefined' &&
!IS_SERVER &&
window['requestIdleCallback']
) {
// delay revalidate if there's cache
// to not block the rendering
window['requestIdleCallback'](softRevalidate)
} else {
softRevalidate()
}
}

闲时

在SWR里面有一个feature,就是当你不在当前页面的时候,就不去进行请求,当onFouse的时候才去做数据请求。事件监听的地方在config.ts里面

// Focus revalidate
let eventsBinded = false
if (typeof window !== 'undefined' && window.addEventListener && !eventsBinded) {
const revalidate = () => {
if (!isDocumentVisible() || !isOnline()) return

for (let key in FOCUS_REVALIDATORS) {
if (FOCUS_REVALIDATORS[key][0]) FOCUS_REVALIDATORS[key][0]()
}
}
window.addEventListener('visibilitychange', revalidate, false)
window.addEventListener('focus', revalidate, false)
// only bind the events once
eventsBinded = true
}

然后对于在过程里面的代码则是这样的:

// whenever the window gets focused, revalidate
let onFocus
if (config.revalidateOnFocus) {
// throttle: avoid being called twice from both listeners
// and tabs being switched quickly
onFocus = throttle(softRevalidate, config.focusThrottleInterval)
if (!FOCUS_REVALIDATORS[key]) {
FOCUS_REVALIDATORS[key] = [onFocus]
} else {
FOCUS_REVALIDATORS[key].push(onFocus)
}
}

也就是说当设置revalidateOnFocus的时候,会给FOCUS_REVALIDATORS推一个function,然后在window监听里面批量执行推的function。

Cache

SWR是这么更新的,在进行useIsomorphicLayoutEffect的时候。会将onUpdate推到CACHE_REVALIDATORS里面。

// register global cache update listener
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
}

// always update error
// because it can be `undefined`
if (stateRef.current.error !== updatedError) {
newState.error = updatedError
needUpdate = true
}

if (needUpdate) {
dispatch(newState)
}

if (shouldRevalidate) {
if (dedupe) {
return softRevalidate()
} else {
return revalidate()
}
}
return false
}

// add updater to listeners
if (!CACHE_REVALIDATORS[key]) {
CACHE_REVALIDATORS[key] = [onUpdate]
} else {
CACHE_REVALIDATORS[key].push(onUpdate)
}

之后根据mutate来执行updates

Return

在useSWR最后return的是一个react memo的object。官网document上面当使用useSWR的时候会返回data, error, mutate, isValidating.

// define returned state
// can be memorized since the state is a ref
useMemo(() => {
const state = { revalidate, mutate: boundMutate } as responseInterface<
Data,
Error
>
Object.defineProperties(state, {
error: {
// `key` might be changed in the upcoming hook re-render,
// but the previous state will stay
// so we need to match the latest key and data (fallback to `initialData`)
get: function () {
stateDependencies.current.error = true
return keyRef.current === key ? stateRef.current.error : initialError
},
enumerable: true
},
data: {
get: function () {
stateDependencies.current.data = true
return keyRef.current === key ? stateRef.current.data : initialData
},
enumerable: true
},
isValidating: {
get: function () {
stateDependencies.current.isValidating = true
return stateRef.current.isValidating
},
enumerable: true
}
})

return state
}, [revalidate])

data和error是从stateRef获取的。从上面的解析中知道,stateRef的值是在cache中获取

mutate

mutate在调用的时候。会走以下的步骤:

  1. 如果传入的data为undefined,则触发CACHE_REVALIDATORS里面的更新
  2. 如果传入的data为function,则执行该function,并传入cache里面的对应的值,返回data
  3. 如果传入的data为promise,则执行promise,返回data
  4. 然后得到的data将设置到cache
  5. 最后,执行未完成的update事件
文章作者: 韦宗圻
文章链接: https://www.weizongqi.com/2020/07/15/SWR%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 wiki
支付宝打赏
微信打赏