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