SWR 幕后工作原理

2025-05-25

SWR 幕后工作原理

我第一次了解SWR是通过 Leigh Halliday 的视频教程:“使用 SWR 通过 Hooks 进行 React 数据获取”。如果你不熟悉 SWR,可以观看 Leigh 的视频、阅读官方文档在 dev.to 上了解更多信息

在这篇文章中,我们将构建我们自己的 SWR 版本,只是为了了解它的工作原理。但首先声明一下:

⚠️警告!
这不是生产代码。它是一个简化的实现,并不包含SWR的所有优秀功能。

在之前的博客文章中,我编写了一个useAsyncFunction钩子来在 React 函数组件中获取数据。该钩子不仅适用于fetch,也适用于任何返回 Promise 的函数。

这是钩子:

type State<T> = { data?: T; error?: string }

export function useAsyncFunction<T>(asyncFunction: () => Promise<T>): State<T> {
  const [state, setState] = React.useState<State<T>>({})

  React.useEffect(() => {
    asyncFunction()
      .then(data => setState({ data, error: undefined }))
      .catch(error => setState({ data: undefined, error: error.toString() }))
  }, [asyncFunction])

  return state
}
Enter fullscreen mode Exit fullscreen mode

如果我们假设这fetchAllGames是一个返回承诺的函数,那么我们就是这样使用钩子:

function MyComponent() {
  const { data, error } = useAsyncFunction(fetchAllGames)
  // ...
}
Enter fullscreen mode Exit fullscreen mode

SWR 具有类似的 API,因此让我们从这个钩子开始,并根据需要进行更改。

更改数据存储

我们可以将数据存储React.useState在模块范围内的静态变量中,而不是存储数据,然后我们可以data从状态中删除该属性:

const cache: Map<string, unknown> = new Map()

type State<T> = { error?: string }
Enter fullscreen mode Exit fullscreen mode

我们的缓存是Map因为否则钩子的不同消费者会用他们不相关的数据覆盖缓存。

key这意味着我们需要向钩子添加一个参数:

export function useAsyncFunction<T>(key: string, asyncFunction: () => Promise<T>) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们改变承诺解决时发生的事情:

asyncFunction()
  .then(data => {
    cache.set(key, data) // <<<<<<<<<<<<< setting cache here!
    setState({ error: undefined })
  })
  .catch(error => {
    setState({ error: error.toString() })
  })
Enter fullscreen mode Exit fullscreen mode

现在我们的“状态”只是错误,所以我们可以简化它。自定义钩子现在如下所示:

const cache: Map<string, unknown> = new Map()

export function useAsyncFunction<T>(
  key: string,
  asyncFunction: () => Promise<T>
) {
  const [error, setError] = React.useState<string | undefined>(undefined)

  React.useEffect(() => {
    asyncFunction()
      .then(data => {
        cache.set(key, data)
        setError(undefined)
      })
      .catch(error => setError(error.toString()))
  }, [key, asyncFunction])

  const data = cache.get(key) as T | undefined
  return { data, error }
}
Enter fullscreen mode Exit fullscreen mode

改变本地数据

这是可行的,但它没有提供改变本地数据或重新加载它的机制。

我们可以创建一个“mutate”方法来更新缓存中的数据,并通过将其添加到返回对象来暴露它。我们希望记住它,这样函数引用就不会在每次渲染时发生变化。(React 文档中关于 useCallback 的内容):

  ...
  const mutate = React.useCallback(
    (data: T) => void cache.set(key, data),
    [key]
  );
  return { data, error, mutate };
}
Enter fullscreen mode Exit fullscreen mode

接下来,为了提供“重新加载”功能,我们提取当前位于我们useEffect的匿名函数内的现有“加载”实现:

React.useEffect(() => {
  asyncFunction()
    .then(data => {
      cache.set(key, data)
      setError(undefined)
    })
    .catch(error => setError(error.toString()))
}, [key, asyncFunction])
Enter fullscreen mode Exit fullscreen mode

再次,我们需要将函数包装在其中useCallback。(有关 useCallback 的 React 文档):

const load = React.useCallback(() => {
  asyncFunction()
    .then(data => {
      mutate(data); // <<<<<<< we call `mutate` instead of `cache.set`
      setError(undefined);
    })
    .catch(error => setError(error.toString()));
}, [asyncFunction, mutate]);

React.useEffect(load, [load]); // executes when the components mounts, and when props change

...

return { data, error, mutate, reload: load };
Enter fullscreen mode Exit fullscreen mode

快到了

整个模块现在看起来像这样:(⚠️但它不起作用)

const cache: Map<string, unknown> = new Map()

export function useAsyncFunction<T>(
  key: string,
  asyncFunction: () => Promise<T>
) {
  const [error, setError] = React.useState<string | undefined>(undefined)

  const mutate = React.useCallback(
    (data: T) => void cache.set(key, data),
    [key]
  );

  const load = React.useCallback(() => {
    asyncFunction()
      .then(data => {
        mutate(data) 
        setError(undefined)
      })
      .catch(error => setError(error.toString()))
  }, [asyncFunction, mutate])

  React.useEffect(load, [load])

  const data = cache.get(key) as T | undefined
  return { data, error, mutate, reload: load }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ 上面的代码不起作用,因为第一次执行时,data是 undefined。之后,promise 解析成功,cache被更新,但由于我们没有使用useState,React 不会重新渲染组件。

厚颜无耻地强制更新

这是一个强制更新我们组件的快速钩子。

function useForceUpdate() {
  const [, setState] = React.useState<number[]>([])
  return React.useCallback(() => setState([]), [setState])
}
Enter fullscreen mode Exit fullscreen mode

我们这样使用它:

...
const forceUpdate = useForceUpdate();

const mutate = React.useCallback(
  (data: T) => {
    cache.set(key, data);
    forceUpdate(); // <<<<<<< calling forceUpdate after setting the cache!
  },
  [key, forceUpdate]
);
...
Enter fullscreen mode Exit fullscreen mode

现在一切正常!当 Promise 解析成功并设置缓存后,组件会被强制更新,并最终data指向缓存中的值。

const data = cache.get(key) as T | undefined
return { data, error, mutate, reload: load }
Enter fullscreen mode Exit fullscreen mode

通知其他组件

这可行,但还不够好。

当多个 React 组件使用此钩子时,只有最先加载的组件或修改了本地数据的组件才会重新渲染。其他组件不会收到任何更改的通知。

SWR 的优点之一是我们不需要设置 React Context 来共享已加载的数据。该如何实现这个功能呢?

订阅缓存更新

我们将cache对象移动到单独的文件中,因为它的复杂性会增加。

const cache: Map<string, unknown> = new Map();
const subscribers: Map<string, Function[]> = new Map();

export function getCache(key: string): unknown {
  return cache.get(key);
}
export function setCache(key: string, value: unknown) {
  cache.set(key, value);
  getSubscribers(key).forEach(cb => cb());
}

export function subscribe(key: string, callback: Function) {
  getSubscribers(key).push(callback);
}

export function unsubscribe(key: string, callback: Function) {
  const subs = getSubscribers(key);
  const index = subs.indexOf(callback);
  if (index >= 0) {
    subs.splice(index, 1);
  }
}

function getSubscribers(key: string) {
  if (!subscribers.has(key)) subscribers.set(key, []);
  return subscribers.get(key)!;
}

Enter fullscreen mode Exit fullscreen mode

请注意,我们不再cache直接导出对象。取而代之的是getCachesetCache函数。但更重要的是,我们还导出了subscribeunsubscribe函数。这些函数用于让我们的组件订阅更改,即使这些更改不是由它们发起的。

让我们更新自定义钩子来使用这些函数。首先:

-cache.set(key, data);
+setCache(key, data);
...
-const data = cache.get(key) as T | undefined;
+const data = getCache(key) as T | undefined;
Enter fullscreen mode Exit fullscreen mode

然后,为了订阅更改,我们需要一个新的useEffect

React.useEffect(() =>{
  subscribe(key, forceUpdate);
  return () => unsubscribe(key, forceUpdate)
}, [key, forceUpdate])
Enter fullscreen mode Exit fullscreen mode

这里我们在组件挂载时订阅特定键的缓存,并unsubscribe在组件卸载时(或者 props 发生变化时)在返回的清理函数中订阅缓存。(React 文档中关于 useEffect 的内容

我们可以稍微整理一下我们的mutate函数。我们不需要forceUpdate从中调用它,因为它现在作为setCache订阅的结果被调用:

  const mutate = React.useCallback(
    (data: T) => {
      setCache(key, data);
-     forceUpdate();
    },
-   [key, forceUpdate]
+   [key]
  );
Enter fullscreen mode Exit fullscreen mode

最终版本

我们的自定义钩子现在看起来像这样:

import { getCache, setCache, subscribe, unsubscribe } from './cache';

export function useAsyncFunction<T>(key: string, asyncFunction: () => Promise<T>) {
  const [error, setError] = React.useState<string | undefined>(undefined);
  const forceUpdate = useForceUpdate();

  const mutate = React.useCallback((data: T) => setCache(key, data), [key]);

  const load = React.useCallback(() => {
    asyncFunction()
      .then(data => {
        mutate(data);
        setError(undefined);
      })
      .catch(error => setError(error.toString()));
  }, [asyncFunction, mutate]);

  React.useEffect(load, [load]);

  React.useEffect(() =>{
    subscribe(key, forceUpdate);
    return () => unsubscribe(key, forceUpdate)
  }, [key, forceUpdate])

  const data = getCache(key) as T | undefined;
  return { data, error, mutate, reload: load };
}

function useForceUpdate() {
  const [, setState] = React.useState<number[]>([]);
  return React.useCallback(() => setState([]), [setState]);
}
Enter fullscreen mode Exit fullscreen mode

此实现不适用于生产环境。它与 SWR 的功能基本相似,但缺少该库的许多优秀功能。

✅ 包含 ❌ 不包括
获取时返回缓存值 删除相同的请求
提供(重新验证)重新加载功能 焦点重新验证
局部突变 按间隔重新获取
滚动位置恢复和分页
依赖抓取
悬念

结论

我认为SWR(或react-queryuseState )是比使用或将获取的数据存储在 React 组件中更好的解决方案useReducer

我继续使用自定义钩子来存储我的应用程序状态useReducer,但useState对于远程数据,我更喜欢将其存储在缓存中。


照片由UmbertoUnsplash上拍摄

文章来源:https://dev.to/juliang/how-swr-works-4lkb
PREV
2020 年的 React 状态管理
NEXT
用于 Web 开发的 CSS 代码生成器终极列表