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