swr 源码解析

Wendell, swrvercelsource codeChinese
Back

不久前知名前端团队 ZEIT 在 twitter 上公布了他们的 data fetching 工具库 swr ,目前这个库已经在 GitHub 上收获了 4,500+ star 。它有着一些非常亦可赛艇的功能,最主要的是实现了 RFC 5861 草案,即发起网络请求之前,利用本地缓存数据进行渲染,待请求的响应返回后再重新渲染并更新缓存,从而提高用户体验。其他功能包括在窗口 focus 时更新、定时更新、支持任意网络请求库、支持 Suspense 等。

本文将会分析其源码以探究它是如何工作的。

代码结构

src
├── config.ts // 配置,同时也定义了一些重要的全局变量
├── index.ts
├── libs // 一些工具函数
│ ├── hash.ts
│ ├── is-document-visible.ts
│ ├── is-online.ts
│ ├── throttle.ts
│ └── use-hydration.ts
├── swr-config-context.ts // 实现 context 支持
├── types.ts // 类型定义
├── use-swr-pages.tsx // 分页
└── use-swr.ts // 主文件

主要流程

我们从官网举的最简单的例子开始:

import useSWR from 'swr'
function Profile () {
const { data, error } = useSWR('/api/user', fetch)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}

我们所使用的 useSWR 函数定义在这里

参数处理阶段useSWR 有多种 function overload,但无论如何都需要传入一个符合 keyInterfacekey 才行。swr 会对开发者传入的参数进行处理,最终得到以下四个重要变量:

然后,swr 会准备一些变量

然后 swr 定义了一个非常重要的函数 revalidate ,这个函数内部定义了发起请求、处理响应和错误的主要过程。

注意生成这个函数的时候调用了 useCallback,依赖项为 key,即只有在 key 发生变化的时候才会重新生成 revalidate 函数。

我们先聚焦于主要流程,此时 shouldDeduping === false

首先会在 CONCURRENT_PROMISES 这个全局变量上缓存 fn 调用后的返回值(一个 Promise),其实这里调用 fn 就已经发起了网络请求。CONCURRENT_PROMISES 这个变量是一个 Map,实际上建立了 key 和网络请求之间的映射,swr 利用这个 Map 来实现去重和超时报错等功能。

很明显能够看出 fn 必须返回一个 Promise,这个简单的约定也使得 swr 能够支持任意的网络请求库,不管是 REST 还是 GraphQL,只要返回 Promise 就行!

然后 revalidate 会等待网络请求完毕,获取到请求数据:

newData = await CONCURRENT_PROMISES[key]

并触发 onSuccess 事件。

接着,更新缓存,并通过 dispatch 方法更新 state ,此时就会触发 React 的重新渲染,重新渲染时就能从 state 里拿到请求数据了。

以上就是 revalidate 函数的主要过程,那么这个函数是在什么时候被调用的呢?我们接着看 useIsomorphicLayoutEffect 的回调函数

useIsomorphicLayoutEffect 函数在服务端就是 useEffect ,在浏览器端就是 useLayoutEffect

首先要判断本次调用时的 key 和上次调用的 key 是否相等。考虑下面这个组件:

const Profile = (props) => {
const { userData, userErr } = useSWR(() => `/${props.userId}`)
}

可以看到即使函数调用的位置相同(Hooks 的正确工作依赖各个 hook 的调用顺序),key 的值也可能不同,所以 swr 必须做这个检验。另外也要判断 data 是否相同,有可能别处更新了 key 所对应的缓存值。总之,当 swr 检查到 key 或者 data 不同,就会执行更新当前的 keydata,并调用 dispatch 进行重绘等操作。

然后,在 revalidate 的基础上定义了 softRevalidate 函数,在 revalidate 执行时执行去重逻辑。

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

然后 swr 就会调用 softRevalidate,如果当前有缓存值且浏览器支持 requestIdleCallback 的话,就作为 requestIdleCallback 的回调执行,避免打断 React 的渲染过程,否则就立即执行。

错误处理

如果数据请求的过程中发生了错误该怎么办呢?

注意到 revalidate 的函数,有很大一部分都在一个 try catch 块中,如果请求出错就会进入 catch 块。

主要做如下几件事情:

默认的重试方法会在当前文档 visible 时执行重试,使用了一个指数退避策略,在一定的时间后重新调用 revalidate

请求去重 (dedupe)

我们在讲解主流程的过程中忽略了很多代码,而这些代码实现了 swr 的一些重要的实用特性,从这个小节开始我会一一讲解。

swr 提供了请求去重的功能,避免某个时间段内重复发起的请求过多。

实现的原理也非常简单。每次 revalidate 函数执行的时候,都会判断是否需要去重

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

即检验 CONCURRENT_PROMISES 里有没有 key 所对应的进行中的请求。

如果 shouldDedupingtrue直接等待请求完成,如果为 false,就按照上文所述进行处理。

revalidateOptsdedupe 属性何时为 true 呢?可以看到声明 softRevalidate 的时候传入了参数:

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

而调用 useSWR 时返回的 revalidate 就是原本的 revalidate ,不带 dedupe 属性。

请求依赖 (dependent fetching)

还是举官网的例子

function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(
() => '/api/projects?uid=' + user.id
)
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}

可见第二个请求依赖于第一个请求,调用 useSWR 时 key 为一个函数,函数体中访问 userid 属性。

swr 通过 getKeyArgs 函数处理 key 为函数的情况,并将调用过程包裹在函数里:

if (typeof _key === 'function') {
try {
key = _key()
} catch (err) {
// dependencies not ready
key = ''
}
}

userundefined 时,获取 undefined.id 出错,key 为空字符串,而 revalidate 函数在 key 为假值时直接返回

if (!key) return false

因此在第一次渲染(MyProjects 函数第一次被调用)时,第二个请求实际上并未发出。而当第一个请求得到响应时,dispatch 会导致组件重绘(MyProjects 函数再次被调用),此时 user 不是 undefined ,第二个请求就能发出了。

所以 swr 所支持的“最大并行请求”的原理非常简单,就是判断能不能获得 key,如果不能获得 key 就用 try catch 语句捕获错误,不发出请求,等待其他请求得到响应后在下次重绘时再试。

请求广播

当请求成功或失败时,都需要调用 broadcastState 函数,这个函数本身非常简单,根据 keyCACHE_REVALIDATORS 中获取一组函数,挨个调用而已:

const broadcastState: broadcastStateInterface = (key, data, error) => {
const updaters = CACHE_REVALIDATORS[key]
if (key && updaters) {
for (let i = 0; i < updaters.length; ++i) {
updaters[i](false, data, error)
}
}
}

这些 updater 是什么?追踪源码可以看到是 onUpdate 函数,可以看到核心就在于下面几行代码:

dispatch(newState)
if (shouldRevalidate) {
return softRevalidate()
}
return false

即更新 state 触发重新渲染,并调用 softRevalidate

所以这个机制的目的是在一个 useSWR 发起的请求得到响应时,刷新所有使用相同 keyuseSWR

Mutate

mutate 是 swr 暴露给用户操作本地缓存的方法,其他部分经过上面的介绍理解起来应该很容易了,关键是如下这行:

MUTATION_TS[key] = Date.now() - 1

这其实是为了抛弃过时的请求用的。

revalidate 函数在执行的时候会记录发起请求的时间

CONCURRENT_PROMISES_TS[key] = startAt = Date.now()

而当请求得到响应时,会判断 mutate 函数调用的时间和发起请求的时间的前后关系

if (MUTATION_TS[key] && startAt <= MUTATION_TS[key]) {
dispatch({ isValidating: false })
return false
}

当发起请求的时间早于 mutate 调用的时间,说明请求已经过期,就抛弃掉这个请求不做任何后处理。

自动轮询 (refetch on interval)

只需要设置一个 timeout 定时调用 softRevalidate 就可以了

Config Context

在 swr 执行的开始,准备 config 对象时调用 useContext 获取 SWRConfigContext

config = Object.assign(
{},
defaultConfig,
useContext(SWRConfigContext),
config
)

Suspense

想要支持 Suspense 很容易,仅需要把数据请求的 Promise 抛出就可以了

throw CONCURRENT_PROMISES[key]

但是和通常情况下不同:当抛出的 Promise 未 resolve 时,React 并不会渲染这部分组件,因此返回值里也无需判断 keyRef.current 是否和 key 相同。

, CC BY-NC 4.0 © Wendell.RSS