深入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 源码,学习核心功能的实现方式,能有效提升代码逻辑思维。
