11

【深度长文】swr 源码精读

 3 years ago
source link: https://zhuanlan.zhihu.com/p/158161974
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【深度长文】swr 源码精读

蚂蚁金服 前端工程师

作者:RichLab 衍良

写在最前:欢迎来到「RichLab」技术专栏,我们将与大家分享前端各领域的高质量技术文章,包括但不限于移动端、小程序、互动技术/数据可视化、Node.js 全栈/中后台、基础架构、个人思考,不限于原创与翻译。

SWR 与 swr 傻傻分不清楚?

SWR 是 stale-while-revalidate 的缩写,stale-while-revalidate(以下简称 SWR)源自 HTTP Cache-Control 协议中的 stale-while-revalidate 指令规范。SWR 作为 HTTP 缓存策略中的一种,它允许先消费缓存中旧(stale)的数据,同时发起新的数据请求(revalidate),当返回最新数据时用最新的数据替换掉运行中的数据。数据请求和替换的过程是异步的,用户无需等待新的数据请求返回时才能看到数据。

swr 库是由 ZEIT 公司(现已更名叫 Vercel,也就是开发出 Next.js 那家公司)出品的 React Hooks 的请求库。和传统意义上的请求库有所不同的是,swr 在数据请求的基础上引入了 SWR 作为缓存策略,同时以 React Hooks 的形式透出 API.


swr 库的特点?

从 swr 库的实现上来看,它的缓存管理和 React 数据流有很多相似之处,至少针对「server 端返回的数据」这方面完全是可以取代数据流了。

swr 库的数据缓存管理原则是这样的:

  • 接受一个缓存 key,同一个 key 在缓存有效期内发起的请求,会走 SWR 策略。
  • 在一定时间内相同 key 下面多次发起的请求,swr 库会做节流,只会有一个请求真正发出去(基于这一点,可以实现跨组件共用一份 server 数据结果)。

除了最核心的数据请求库和缓存管理之外,swr 库还提供了丰富的扩展配置, 属于一个配置驱动的基础工具库:

  • 请求出错自动重试,以及重试次数等配置
  • 窗口重新聚焦(如 PC 的 tab 切换、手机设备的视图切换)时重新请求数据
  • 网络重连时重新请求数据
  • 轮询自动刷新数据
  • 可自定义请求节流的时间间隔
  • Suspense Mode 支持


swr 库源码精读

useSWR API
说明:React custom hook,实现数据管理和 server 取数等核心逻辑。

配置简析
useSWR 函数签名:

function useSWR<Data = any, Error = any>(
  key: keyInterface
): responseInterface<Data, Error>;
function useSWR<Data = any, Error = any>(
  key: keyInterface,
  config?: ConfigInterface<Data, Error>
): responseInterface<Data, Error>;
function useSWR<Data = any, Error = any>(
  key: keyInterface,
  fn?: fetcherFn<Data>,
  config?: ConfigInterface<Data, Error>
): responseInterface<Data, Error>;

1、key——管理数据的唯一依据

key即上文提到的 SWR 策略中的缓存 key, 它是 useSWR 管理数据的唯一依据,具有相同的key的所有 hooks 会共享同一份数据。

type keyInterface = (() => string | any[] | null) | string | any[] | null;

key类型定义上看,它可以是字符串、任意数组或者 null,也可以是一个返回上述类型的函数。通过阅读源码可知,key参数会被 cache.serializeKey 格式化处理:

function serializeKey(key: keyInterface): [key: string, args: any, errorKey: string]

作为使用者,你可能有必要了解一下serializeKey函数内部做了什么:

assert(serializeKey('foo') === ['foo', null, 'err@foo']); // string as key type
assert(serializeKey(null) === ['', null, '']); // null as key type

// any[] as key type
const [ key, args, errorKey ] = serializeKey(['bar', { v: 'hello' }, ['world']]);
assert(key && typeof key === 'string'); // 具体值是什么无需关注,但一定是字符串
assert(args === ['bar', 'hello', ['world']]);
assert(errorKey && typeof errorKey === 'string');
  • serializeKey函数输出的key是一个字符串,它就是useSWR最终用于存储数据的键名。这里比较有意思的是对 Array key 的处理,serializeKey内部会对 Array key 进行 hash 转换,生成一个唯一的字符串,感兴趣的同学可以参阅源码
  • errorKeykey一一对应,是useSWR用于存储请求的 error 对象的键名。

PS: useSWR 对于 Array key 的处理,其实不够完美,比如当数组项是 object 的时候,数值项的顺序不同会导致 hash 转换的 key 不同,从而导致 useSWR hooks 间的数据共享失效,所以,请尽量保证传入的 key 是字符串吧。

2、fetcher定义和配置(config):

fetcher是具体发出请求的方法,由使用者自定义。此处简言之就是 fetcher 方法可以通过第 2 个参数传入,也可以作为 config 配置项传入,处理过程略过。

config 模块可以简单了解下,帮助理解下文的源码解析,它会 export 以下内容:

  • defaultConfig:内置配置,可以通过useSWR的第 2 或第 3 个参数传入覆盖,所有可配置项在官方文档都已经列举出来了。
  • CACHE_REVALIDATORS:全局变量,以 key 为键名存储useSWR hook 的 updater 队列,可用于在 React 组件外更新组件。
  • CONCURRENT_PROMISES:全局变量,存储当前 key 发起的数据请求 promise 实例。
  • CONCURRENT_PROMISES_TS:全局变量,存储当前 key 发起的数据请求时间戳。
  • MUTATION_TS:全局变量,存储当前 key 发起的 mutate 更新时间戳。
  • FOCUS_REVALIDATORS:全局变量,以 key 为键名存储 focus handler 队列,在窗口聚焦时会顺序调用 focus handler 队列(执行数据更新)。
  • cache:一个简易的 cache 对象,定义了数据的 get/set 等方法,内部以 HashMap 的格式存储 key 对应的数据。
type responseInterface<Data, Error> = {
  data?: Data
  error?: Error
  revalidate: () => Promise<boolean>
  mutate: (
    data?: Data | Promise<Data> | mutateCallback<Data>,
    shouldRevalidate?: boolean
  ) => Promise<Data | undefined>
  isValidating: boolean
}
  • data(React state):fetch 的数据结果
  • error(React state):fetch 的错误信息
  • isValidating(React state):fetch 的状态
  • revalidate API:数据更新(取数)API,fetcher wrapper
  • mutate API:全局 mutate API bounding,无需再传入key参数

初始化阶段

1、首先是config的获取,分为三部分,除了在useSWR调用时传入,还可以通过全局的SWRConfigContext Provider 统一配置:

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

其次,useSWR初始返回值(data)的获取,是优先读取缓存中的数据,体现出 SWR 策略中「缓存优先」原则:

const initialData = cache.get(key) || config.initialData;

2、接下来是useSWR自定义 rerender 部分(出参中的dataisValidatingerror三个都属于 React state,自定义 rerender 是为了实现合并更新)。实现合并更新通常我们的做法是用 useReducer hook 即可,而useSWR是使用 ref + forceRender 的方式,使用useRef保存当前 hook 的状态引用,可以和传入的新的 state 进行对比(对比算法参考compare 参数),只有数据发生变更时才触发 rerender, 这是性能优化的考虑。

useSWR定义的 ref 引用:

// 保存当前 hook 的状态
const stateRef = useRef({
  data: initialData,
  error: initialError,
  isValidating: false
});
// 当前组件是否已经 unmount
const unmountedRef = useRef(false);
// key 的引用,useSWR 允许在运行时修改传入的 key
const keyRef = useRef(key);

3、定义数据更新行为(revalidation):

type function revalidate({ retryCount?: number; dedupe?: boolean }): Promise<boolean>;

revalidate函数是useSWR的核心逻辑之一,它完整定义了一次数据更新的过程:

解释下其中几处关键的分支判断:

  • shouldDeduping:表示是否 fetch 去重,传入了dedupe参数且当前同名 key 下面有还未返回的 fetch 请求。
  • shouldIgnoreRequest:是否忽略当前 request response, 它定义了多个 request 之间结果如何取舍,以及 request response 和本地更新(下文的 mutate API)的优先级,源码里边的示意图看起来比较直观:
// case 1:同时发起了多个 request,只有最后一个 request 的结果有效(可以理解为debounce)
req1------------------>res1        (current request)
       req2------------------->res2
    
// case 2:在 request 发起到返回期间,有本地更新(mutate)发生,会忽略本次 request response
req------------------------>res
       mutate------>end

// case 3:mutate 发生在 request 之前,且在 request 返回之前返回,会忽略本次 request response
       req---------------->res
mutate----------->end

// case 4:request 发起期间有 mutate 发生,且 request 先返回,会忽略本次 request response 并等待 mutate 返回
req------------------>res
       mutate-------...---------->

mount 阶段

1、处理数据更新(state 一致性校验)
useSWR允许修改key,当组件mount阶段检测到key和初始化时不一致时,或者当前组件的stateRef存储的data和缓存取到的data不一致(执行config.compare)时,会强制更新组件:

// (部分)核心代码
if (
  keyRef.current !== key ||
  !config.compare(currentHookData, latestKeyedData)
) {
  dispatch({ data: latestKeyedData }) // 刷新data
  keyRef.current = key
}

此处的latestKeyedData优先从全局缓存cache[key]读取(如其他组件以同名 key 调用过了useSWR),其次是取config.initialData配置。

2、(自动)发起数据更新

根据 SWR“优先消费缓存中的旧数据,在后台同步发起数据更新”的策略,在默认情况下,useSWR会在 mount 时主动发起一次数据更新(revalidate)。此外,useSWR内部还做了适当优化,在进程空闲时(requestIdleCallback)发起数据更新。

当然,我们也可以根据需要,忽略useSWR在 mount 后立即发起数据更新,涉及的配置是initialDatarevalidateOnMount

// (部分)核心代码
if (
  config.revalidateOnMount ||
  (!config.initialData && config.revalidateOnMount === undefined)
) {
	window['requestIdleCallback'](() => revalidate(...))
}

社区一些 request hook, 如 umijs/useRequest 实现的手动模式,通过这两个配置即可实现。

3、处理窗口聚焦时重新取数

这个实现比较简单,在FOCUS_REVALIDATORS全局变量以key为键名,存储revalidate引用,然后在visibilitychange事件 handler 执行所有的revalidate,从而实现当窗口从失焦到聚焦(且网络在线)时,重新发起新的数据更新。当然了,前提是配置传入了revalidateOnFocus才会开启。

// (部分)核心代码
if (!FOCUS_REVALIDATORS[key]) {
  FOCUS_REVALIDATORS[key] = [revalidate]
} else {
  FOCUS_REVALIDATORS[key].push(revalidate)
}

// 监听 visibilitychange
const revalidate = () => {
  if (!isDocumentVisible() || !isOnline()) return

  for (let key in FOCUS_REVALIDATORS) {
    if (FOCUS_REVALIDATORS[key][0]) FOCUS_REVALIDATORS[key][0]()
  }
}
window.addEventListener('visibilitychange', revalidate, false)

类似地,断网重连时触发重新取数的逻辑也类似,监听的是online事件,具体实现就不展开分析了。

4、全局监听数据更新

useSWR允许在 React 组件外更新状态,通过CACHE_REVALIDATORS全局变量存储 update handler, 接受传入新的 state, 在 update handler 里进行 data diff 后决定是否更新state.

// (部分)核心代码
const onUpdate = (updatedData, updatedError) => {
  const newState = {}
  let needUpdate = false
  
  // data diff
  if (config.compare(stateRef.current.data, updatedData)) {
    newState.data = updatedData
    needUpdate = true
  }
  // set error
  if (stateRef.current.error !== updatedError) {
    newState.error = updatedError
    needUpdate = true
  }
  
  if (needUpdate) {
    dispatch(newState) // force render
  }
}

if (!CACHE_REVALIDATORS[key]) {
  CACHE_REVALIDATORS[key] = [onUpdate]
} else {
  CACHE_REVALIDATORS[key].push(onUpdate)
}

这其实是一种 local update 行为,你完全可以先手动 fetch 数据,然后写入到useSWR state. swr 库已经封装成一个单独的 API 了,即后面的mutate API.

5、轮询(polling)

轮询的实现比较简单,与之相关的3个配置是refreshIntervalrefreshWhenHiddenrefreshWhenOffline,当配置传入之后,通过 setTimeout 不间断地调用revalidate实现自动更新。

mutate API

说明:本地更新(local mutation)cache中存储的 state,它只是一个纯 js 函数,所以调用不受 React Hooks Rules 的限制。

type mutate<Data> = (
  key: keyInterface,
  data: Data | Promise<Data> | ((currentValue: Data) => Promise<Data> | Data),
  shouldRevalidate?: boolean
) => Promise<Data>

从函数签名可以大致猜出来mutate API 内部的逻辑了:

  • 用传入的 data(或等待其返回的结果),更新 cache[key]
  • 通知所有通过useSWR持有此 key state 的组件,实现组件的视图更新(参考上一节的「全局监听数据更新」)
  • 如果shouldRevalidate参数为 true, 则组件视图在更新的同时会发起 revalidate, 从 server 端取数并更新视图(如果数据有变化)

mutate 是一个很有用的 API, 抛开 Hooks 来讲,它就是 cache 对象的 setter wrapper. 通常情况下,对于 server 数据我们无需修改,本地和 server 端保持一致。在某些场景下,我们可以通过mutate在本地先修改存储在 cache 中的 state, 在页面刷新时再重新从 server 取数,从而实现体验优化。或者在useSWR调用之前先通过mutate写入 cache, 替代initialData.

trigger API

说明:在 React 组件外触发 revalidate(因useSWR已经在CACHE_REVALIDATORS全局变量存储了 update handler),它同样是一个纯 js 函数,不受 React Hooks Rules 的限制。

type trigger = (key: keyInterface, shouldRevalidate?: boolean) => Promise<any>

trigger的实现比较简单,即根据传入的key找到相应的 update handler 队列、从cache中取出key对应的 state, 遍历并执行 update handler, 从而实现将最新的 state 更新到组件视图。

useSWRInfinite(useSWRPages) API

说明:分页的实现,之前叫useSWRPages,最新版本已经改成了useSWRInfinite,这个 API 还不是很稳定,暂时不展开解读。


感谢你看到最后~

我们是蚂蚁金服 RichLab 前端团队,目前正在急招 P6-P7 的前端工程师

我们的主要业务是 花呗 和 借呗,除了业务好待遇高,技术也是百花齐放

有负责蚂蚁互动小组的 Web 3D 引擎 & 工作流,走在 Web 3D 互动前沿:

有在 D2 上大放异彩的浏览器实时构建技术,完全基于浏览器技术实现的预览和调试解决方案:

再挑几个颇具代表性的技术产品给你看看:

除了上述这些技术产品之外,智能化业务体验平台、Serverless(SFF)、工程化等技术领域,甚至 旅行音乐Vlog 等生活娱乐领域也都是我们团队的专长~

RichLab 前端团队目前已有 50+ 人,分布在杭州、北京、重庆

如果你对以上技术感兴趣,或者想要和我们一起实现普惠金融

欢迎私信联系我,或者投递简历到 [email protected]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK