深入SWR 设计与源码分析
1 前言
SWR
由 Next.js
(React SSR
框架)背后的同一团队创建。号称最牛逼的React
数据请求库
SWR
:是stale-while-revalidate
的缩写 ,源自 HTTP Cache-Control
协议中的 stale-while-revalidate 指令规范
。也算是HTTP
缓存策略的一种,这种策略首先消费缓存中旧(stale
)的数据,同时发起新的请求(revalidate
),当返回数据的时候用最新的数据替换运行的数据。数据的请求和替换的过程都是异步的,对于用户来说无需等待新请求返回时才能看到数据。
SWR
的缓存策略:
接受一个缓存
key
,同一个key
在缓存有效期内发起的请求,会走SWR
策略
在一定时间内,同一个
key
发起多个请求,SWR
库会做节流,只会有一个请求真正发出去
举个官网的简单列子
import useSWR from 'swr'
function Profile() {
const { data, error, isValidating, mutate } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
这个例子是前端较为基础的请求,通过使用useSWR
实现了简单明了的请求,当然它还有很多更强大的功能。
2 基础用法
2.1 useSWR
const { data, error, isValidating, mutate } = useSWR(key, fetcher, options)
2.1.1 参数
useSWR
接受三个参数:一个 key
、一个异步请求函数 fetch
和一个 config
配置 。
key
: 请求的唯一key string
(或者是function
/array
/null
) 是数据的唯一标识符,标识数据请求,通常是API URL
,并且fetch
接受key
作为其参数。key
为函数function
或者null
:可以用来有条件地请求数据实现按需请求,当函数跑出错误或者falsy
值时,SWR
将不会发起请求。// 有条件的请求
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// ...或返回一个 falsy 值
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
// ... 或在 user.id 未定义时抛出错误
const { data } = useSWR(() => '/api/data?uid=' + user.id, fetcher)依赖请求场景:当需要一段动态数据才能进行下一次数据请求时,它可以确保最大程度的并行性(
avoiding waterfalls
)以及串行请求。function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
// 传递函数时,SWR 会用返回值作为 `key`。
// 如果函数抛出错误或返回 falsy 值,SWR 会知道某些依赖还没准备好。
// 这种情况下,当 `user`未加载时,`user.id` 抛出错误
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
fetcher(args)
: 返回数据的异步函数,接受key
做参数并返回数据,你可以使用原生的fetch
或Axios
之类的工具。
config
2.1.2 返回值
data
: 通过fetcher
用给定的key
获取的数据(如未完全加载,返回undefined
,这时可以用来做一些loading
态)
error
:fetcher
抛出的错误(或者是undefined
)
isValidating
: 是否有请求或重新验证加载
mutate(data?, shouldRevalidate?)
: 更改缓存数据的函数,可以在数据更改发起数据重新验证的场景
可以使用 useSWRConfig()
所返回的 mutate
函数,来广播重新验证的消息给其他的 SWR hook(*)
。使用同一个 key
调用 mutate(key)
即可。以下示例显示了当用户点击 “注销” 按钮时如何自动重新请求登录信息
import useSWR, { useSWRConfig } from 'swr'
function App () {
const { mutate } = useSWRConfig()
return (
<div>
<Profile />
<button onClick={() => {
// 将 cookie 设置为过期
document.cookie = 'token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
// 告诉所有具有该 key 的 SWR 重新验证
mutate('/api/user')
}}>
Logout
</button>
</div>
)
}
通常情况下 mutate
会广播给同一个 cache provider
下面的 SWR hooks
。如果没有设置 cache provider
,即会广播给所有的 SWR hooks
。
2.2 核心特性
通过 SWR
官网介绍,SWR
有以下比较亮点特性
极速、轻量、可重用的 数据请求
内置 缓存 和重复请求去除
间隔轮询
聚焦时、网络恢复时重新验证
本地缓存更新
智能错误重试
分页和滚动位置恢复
3 核心功能拆解实现
虽然 SWR
特性很多,功能很强大,能极大提升用户体验,但是 SWR
却使用很简洁的思路完成上述所有的功能,并通过一个hook useSWR
即可拥有几乎全部功能。即功能强大,api
使用却十分简单,开发体验十分喜人。
上面说的 SWR
更多是功能特性,体验优化。那么站在技术的角度,SWR
又是扮演什么角色?
纯纯的
hook
请求库
全局状态管理
数据缓存
自动化重新数据验证(轮询、断网重连、页面聚焦等)
下面我们拆解他的功能,按照 SWR
一样的思路代码实现相同
3.1 hooks 请求库
useSWR
是一个 react hook
,通过这个 hook
你可以获取数据,并在数据获取后,触发页面重新渲染,这是 hook
基本特性,平平无奇,react
useState
+ promise
就能轻松实现。
function useSwr(key, fetcher) {
const [state, setState] = useState({})
const revalidate = useCallback(async () => {
try {
const result = await fetcher(key)
setState({
error: undefined,
data: result
})
} catch (error) {
setState({
...state,
error
})
}
})
useEffect(() => {
revalidate()
}, [key])
return { data, error }
}
有点简单,也的确没亮点,虽然 SWR
里面允许 key
是 string
、array
甚至 function
。但觉得这些都不是亮点。但是.... SWR
有一个小操作,有点按需更新的意思
function App() {
// const { data, error, isValidating } = useSWR('/api/user', fetcher)
const { data } = useSWR('/api/user', fetcher)
return <div>hello {data.name}!</div>
}
useSWR
会返回 data, error, isValidating
,只要有一个变化页面就会重新渲染。可页面只用到 data
, 是否可以 仅仅 data
更改时候才触发重新渲染呢?
SWR
做了一个操作,有点 vue mvvm
模型的意思(setter
,getter
在脑海里琅琅上口🤔)。React
的setState
会触发更新,直接使用肯定不行,SWR
就封装了一下,SWR
是在 state.js
里面实现该逻辑
function useStateWithDeps(state) {
const stateRef = useRef(state)
//用于存储哪些属性被订阅
const stateDependenciesRef = useRef({
data: false,
error: false,
isValidating: false
})
const rerender = useState({})[1]
const setState = useCallback((payload) => {
let shouldRerender = false
const currentState = stateRef.current
for (const k in payload) {
// 是否有变化
if (currentState[k] !== payload[k]) {
currentState[k] = payload[k]
// 是否有被使用
if (stateDependenciesRef.current[k]) {
shouldRerender = true
}
}
}
if (shouldRerender && !unmountedRef.current) {
rerender({})
}
})
useEffect(() => {
stateRef.current = state
})
return [stateRef, stateDependenciesRef.current, setState]
}
// 如果单纯设计 stateDependenciesRef,可以把setter、getter 写在 useStateWithDeps 里面。但use 并没有直接暴露 stateDependenciesRef,而是暴露 useSwr。所以把数据劫持放在 useSwr
function useSwr(key, fetcher) {
//......
const [stateRef, stateDependencies, setState] = useStateWithDeps({
data,
error,
isValidating
})
return {
get data() {
stateDependencies.data = true
return data
},
get error() {
stateDependencies.error = true
return error
},
get isValidating() {
stateDependencies.isValidating = true
return isValidating
}
}
}
3.2 全局状态管理
SWR
可不是简单管理一个组件的状态,而是组件之间相同 key
直接的数据是可以保持同步刷新,牵一发而动全身。React
的 useState
使用就是只会触发使用组件的重新渲染,即谁用我,我就更新谁。那么如何做到组件之间,一个地方修改,所有地方都能触发重新渲染。
下面演示,精简版的 React
全局状态库数据管理的实现。SWR
底层逻辑与之不谋而合
import { useState, useEffect } from 'react'
//全局数据存储
let data = {}
//发布订阅机制
const listeners = []
function broadcastState(state) {
data = {
...data,
...state,
}
listeners.forEach((listener) => listener(data))
}
const useData = () => {
const [state, setState] = useState(data)
function handleChange(payload) {
setState({
...state,
...payload,
})
broadcastState(payload)
}
useEffect(() => {
listeners.push(handleChange)
return () => {
listeners.splice(listeners.indexOf(handleChange), 1)
}
}, [])
return [state, handleChange]
}
export default useData
3.3 数据缓存
在上面讲到全局状态时候,我们定义了一个 data
存储了数据,在 SWR
底层,则是采用一个 weakMap
存储数据,道理相似。
SWR
是一个请求库,对于数据存储,并不是直接存储 Data
, 而是存储 Promise<Data>
充分利用promise
状态一旦更改就不会变的特性,也十分适合异步数据请求
//区分 key,可以理解为安置 key 管理
const globalState = new Map({
//更新数据事件,即上文中的 listeners
STATE_UPDATERS: {}, //[key:callbacks]
//重新获取数据事件
EVENT_REVALIDATORS: {}, //[key:callbacks]
// 异步数据请求缓存,缓存的是 promise
FETCH: {}, //[key:callbacks]
})
function useSwr(key, fetcher) {
//...
const [stateRef, stateDependencies, setState] = useStateWithDeps(cacheInfo)
//获取数据函数
const revalidate = async () => {
if (cache) {
} else {
// 没有 await
const fetch=fetcher(...args)
setCache(key,fetch)
}
}
}
3.4 自动化重新数据验证(轮询、断网重连、页面聚焦等)
3.4.1 轮询数据
即间隔固定时间,重新发送请求,更新数据
function useSwr(key,fetcher) {
//...
// Polling
useEffect(() => {
let timer
function next() {
timer = setTimeout(execute, interval)
}
function execute() {
revalidate().then(next)
}
next()
return () => {
if (timer) {
clearTimeout(timer)
timer = -1
}
}
}, [interval])
}
3.4.2 断网重连、页面聚焦重新请求
也是如同上文的的全局状态管理,在使用 useSwr
时候把重新获取数据的函数(事件)推送到全局的数据存储里面,然后订阅浏览器事件,并从全局数据存储里面读取事件执行
//subscribe-key.js
function subscribeCallback(events, callback) {
events.push(callback)
return () => {
const index = events.indexOf(callback)
// 释放事件
if (index >= 0) {
// O(1): faster than splice
events[index] = events[events.length - 1]
events.pop()
}
}
}
// useSwr.js
function useSwr(key, fetcher) {
//...
//获取数据函数
const revalidate = async () => {
//...fetcher()
}
useEffect(() => {
// 更新数据,推入队列,确保其他组件更新数据,能通过 broadcastState 触发当前组件更新
const onStateUpdate = () => {
//... setState()
}
// 重新刷新数据,在一些网络恢复、聚焦时候执行
const onRevalidate = () => {
//... revalidate()
}
const unsubUpdate = subscribeCallback(key, STATE_UPDATERS, onStateUpdate)
const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate)
return () => {
unsubUpdate()
unsubEvents()
}
}, [key, revalidate])
//...
}
浏览器订阅事件如下
在 useEffect
统一监听浏览器事件即可
// web-preset.js
const onWindowEvent =window.addEventListener
const onDocumentEvent = document.addEventListener.bind(document)
const offWindowEvent =window.removeEventListener.bind(window)
const offDocumentEvent =document.removeEventListener.bind(document)
const initFocus = (callback) => {
// 页面重新聚焦 重新获取数据
onDocumentEvent('visibilitychange', callback)
onWindowEvent('focus', callback)
return () => {
offDocumentEvent('visibilitychange', callback)
offWindowEvent('focus', callback)
}
}
const initReconnect = (callback) => {
// 网络恢复,重新获取数据
const onOnline = () => {
online = true
callback()
}
// nothing to revalidate, just update the status
const onOffline = () => {
online = false
}
onWindowEvent('online', onOnline)
onWindowEvent('offline', onOffline)
return () => {
offWindowEvent('online', onOnline)
offWindowEvent('offline', onOffline)
}
}
3.5 其他
3.5.1 全局配置
useSWR(key, fetcher, options)
中options
支持需要配置属性,那么如果期望在某个范围内,所有的hook
,共用一套配置如何实现呢。SWR
提供一个组件叫 SwrConfig
import useSWR, { SWRConfig } from 'swr'
function App () {
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (resource, init) => fetch(resource, init).then(res => res.json())
}}
>
<Dashboard />
</SWRConfig>
)
}
Dashboard
下所有的 useSwr
共用 value
作为配置。
组件提供全局配置的 provider
,子组件都共用这个配置,是一种很常见组件的设计思路。主要思路就是利用 react.createContext
提供 Provider
、Consumer
能力,不过现在使用 useContext
,使用上会比 Consumer
好太多了。
const SWRConfigContext = createContext({})
const ConfigProvider = (props) => {
// mergeConfigs 会处理中间件 merge逻辑
// 必须继承上一个 provider SWRConfig 的配置 进行 merge
const extendedConfig = mergeConfigs(useContext(SWRConfigContext), value)
return createElement(
SWRConfigContext.Provider,
mergeObjects(props, {
value: extendedConfig, // swr 一些运算处理的配置
})
)
}
export const useSWRConfig = () => {
return mergeConfigs(defaultConfig, useContext(SWRConfigContext))
}
export const SWRConfig = OBJECT.defineProperty(ConfigProvider, 'default', {
value: defaultConfig,
})
然后在使用中就可以使用全局配置
const fallbackConfig = useSWRConfig()
// 格式化用户入参
const [key, fn, _config] = normalize(args)
const config = mergeConfigs(fallbackConfig, _config)
3.5.2 中间件-洋葱模型
SWR
也支持中间件,让你能够在 SWR hook
之前和之后执行代码。
useSWR(key, fetcher, { use: [a, b, c] })
中间件执行的顺序是 a → b → c
,如下所示:
enter a
enter b
enter c
useSWR()
exit c
exit b
exit a
那么 swr 是如何实现洋葱模型的呢?代码简单只有10行不到的代码。就是实现一个 compose
逻辑,然后通过函数执行栈一层层嵌套即可,这里有个注意点就是,从最后一个开始嵌套,然后从第一个开始执行。逐层释放执行栈,则刚好是完美洋葱模型的执行顺序。
一个中间件格式如下:
接受上一个 useSwr
这个hook
,返回一个新的 hook
。 很符合 compose
函数的思想呀
// Apply middleware
let next = hook //原始的中间件
const { use } = config //中间件列表
if (use) {
for (let i = use.length; i-- > 0; ) {
next = use[i](next)
}
}
return next(key, fn || config.fetcher, config)
3.5.3 请求时序问题处理
这个其实逻辑很简单,但却很关键,所以也在这说明一下
假设我们对一个 key
,发了2个请求req1
、req2
。发出的顺序和数据返回数据如下
// req1------------------>res1 (current one)
// req2---------------->res2
因为 req2
发出的事件比较晚,那么我们页面展示的数据应该是以 res2
。即始终只更新最晚一次请求的返回值,即 req2
的返回值(这里就算 res2
返回更早也是展示 res2
,取决于请求事件)
function useSwr(key, fetcher) {
const revalidate = async () => {
FETCH[key] = [currentFetcher(...fnArgs), getTimestamp()]
;[newData, startAt] = FETCH[key]
newData = await newData
//...
// 当请求数据返回时候,发现staryAt 不一致,说明有其他同 key 请求已经 发出去
if (!FETCH[key] || FETCH[key][1] !== startAt) {
//!(FETCH[key] && FETCH[key][1] == startAt)
if (shouldStartNewRequest) {
if (isCurrentKeyMounted()) {
getConfig().onDiscarded(key)
}
}
return false
}
}
}
这里有一个容易疑惑的点就是为何只是判断 startAt
不相等就放弃当前数据更改呢?这是因为 FETCH
是全局缓存,是用 map
存储,实时更新。且 FETCH[key]
始终只存一个请求,一旦不等就说明在此之后有相同的 key
请求被发出。
startAt
这个变量是存储在当前组件的作用域里面,而 FETCH
全局缓存,所有组件共享的数据
3.5.4 工具函数
SWR
里面还有需要工具函数可以学习
3.5.4.1 hash
与深比较
SWR
中的hash.js
用于哈希 key
、data
,形成一个字符串,并在深比较函数 compare
通过哈希后字符串判断数据是否有变化,是否需要重新请求、重新渲染
3.5.4.2 参数格式化处理
SWR
的key
格式可以是 function / array / null
,也是在统一的 normalize.js
里做处理,如果是 falsy
值,则表示不发请求
4 源码分析
SWR
还有许多 options
配置和功能,比如上轮询间隔、是否启用缓存、是否开重复请求去除、错误重试、超时重试、支持 ssr
等。这些都不影响主流逻辑,下面我们按照上面拆解的核心功能,查看 SWR
源码。
4.1 目录结构
SWR
对把逻辑拆分到一个个文件,通过文件名以及我们上面的分析,很容易猜出文件中的逻辑
├── constants
│ └── revalidate-events.ts
├── index.ts
├── types.ts
├── use-swr.ts
└── utils
├── broadcast-state.ts // 组件状态修改通知其他组件渲染
├── cache.ts // 缓存,缓存事件:如重新请求、网络恢复等事件
├── config-context.ts //全局配置 react context
├── config.ts
├── env.ts
├── global-state.ts //缓存,搭配 cache 使用
├── hash.ts // 对数据hash,形成字符串,用于深比较
├── helper.ts
├── merge-config.ts
├── mutate.ts // 更改缓存
├── normalize-args.ts // 格式化入参
├── resolve-args.ts //初始化操作,是一个 hoc 逻辑,
├── serialize.ts // hash
├── state.ts // 属性按需触发重新渲染
├── subscribe-key.ts // 添加事件订阅
├── timestamp.ts
├── use-swr-config.ts
├── web-preset.ts //浏览器事件:聚焦、网络状态变更
└── with-middleware.ts //中间件
4.2 核心源码
4.2 核心源码
核心流程图
src/use-swr.ts
function useSwr(args) {
//...
const fallbackConfig = useSWRConfig()
// 格式化用户入参
const [key, fn, _config] = normalize(args)
const config = mergeConfigs(fallbackConfig, _config)
// 读取全局缓存,如数据缓存(promise)、事件缓存
const [EVENT_REVALIDATORS, STATE_UPDATERS, MUTATION, FETCH] =
SWRGlobalState.get(cache)
//当前 key 读取存储,有缓存优先使用缓存数据
const cached = cache.get(key)
const data = isUndefined(cached) ? fallback : cached
const info = cache.get(keyInfo) || {}
const error = info.error
//按需更新
const [stateRef, stateDependencies, setState] = useStateWithDeps({
data,
error,
isValidating,
})
//获取数据函数
const revalidate = async () => {
const shouldStartNewRequest = !FETCH[key] || !opts.dedupe
if (!shouldStartNewRequest) {
} else {
// 没有 await
FETCH[key] = [currentFetcher(...fnArgs), getTimestamp()]
}
//...
;[newData, startAt] = FETCH[key]
newData = await newData
//...
finishRequestAndUpdateState()
//...
broadcastState()
}
useEffect(() => {
// 更新数据,推入队列,确保其他组件更新数据,能通过 broadcastState 触发当前组件更新
const onStateUpdate = () => {
//... setState()
}
// 重新刷新数据,在一些网络恢复、聚焦时候执行
const onRevalidate = () => {
//... revalidate()
}
const unsubUpdate = subscribeCallback(key, STATE_UPDATERS, onStateUpdate)
const unsubEvents = subscribeCallback(key, EVENT_REVALIDATORS, onRevalidate)
return () => {
unsubUpdate()
unsubEvents()
}
}, [key, revalidate])
return {
get data() {
stateDependencies.data = true
return data
},
get error() {
stateDependencies.error = true
return error
},
get isValidating() {
stateDependencies.isValidating = true
return isValidating
},
}
}
5 总结
SWR
是 一个很轻的 hook
请求库,能在提升用户体验的前提下,也保证很好的开发体验和很低的开发成本。设计理念也很 React
,核心功能的实现逻辑也很简单。通过分析SWR
源码,学习核心功能的实现方式,能有效提升代码逻辑思维。