swr 源码解析

Wenzhao,swrvercelsource codeChinese

不久前知名前端团队 ZEIT 在 twitter (opens in a new tab) 上公布了他们的 data fetching 工具库 swr (opens in a new tab) ,目前这个库已经在 GitHub (opens in a new tab) 上收获了 4,500+ star 。它有着一些非常亦可赛艇的功能,最主要的是实现了 RFC 5861 草案 (opens in a new tab),即发起网络请求之前,利用本地缓存数据进行渲染,待请求的响应返回后再重新渲染并更新缓存,从而提高用户体验。其他功能包括在窗口 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 函数定义在这里 (opens in a new tab)

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

然后,swr 会准备一些变量 (opens in a new tab)

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

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

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

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

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

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

newData = await CONCURRENT_PROMISES[key]

并触发 onSuccess 事件。

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

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

useIsomorphicLayoutEffect 函数 (opens in a new tab)在服务端就是 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 (opens in a new tab),如果当前有缓存值且浏览器支持 requestIdleCallback 的话,就作为 requestIdleCallback 的回调执行,避免打断 React 的渲染过程,否则就立即执行。

错误处理

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

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

主要做如下几件事情:

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

请求去重 (dedupe)

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

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

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

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

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

如果 shouldDedupingtrue直接等待请求完成 (opens in a new tab),如果为 false,就按照上文所述进行处理。

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

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

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

请求依赖 (dependent fetching)

还是举官网的例子 (opens in a new tab)

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 函数 (opens in a new tab)处理 key 为函数的情况,并将调用过程包裹在函数里:

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

userundefined 时,获取 undefined.id 出错,key 为空字符串,而 revalidate 函数在 key 为假值时直接返回 (opens in a new tab)

if (!key) return false

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

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

请求广播

当请求成功或失败时,都需要调用 broadcastState (opens in a new tab) 函数,这个函数本身非常简单,根据 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 函数 (opens in a new tab),可以看到核心就在于下面几行代码:

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

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

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

Mutate

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

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

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

revalidate 函数在执行的时候会记录发起请求的时间 (opens in a new tab)

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

而当请求得到响应时,会判断 mutate 函数调用的时间和发起请求的时间的前后关系 (opens in a new tab)

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

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

自动轮询 (refetch on interval)

只需要设置一个 timeout 定时调用 softRevalidate 就可以了 (opens in a new tab)

Config Context

在 swr 执行的开始,准备 config 对象时调用 useContext 获取 SWRConfigContext (opens in a new tab)

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

Suspense

想要支持 Suspense 很容易,仅需要把数据请求的 Promise 抛出就可以了 (opens in a new tab)

throw CONCURRENT_PROMISES[key]

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

, CC BY-NC 4.0 © Wenzhao.