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
}
如果我们假设这fetchAllGames
是一个返回承诺的函数,那么我们就是这样使用钩子:
function MyComponent() {
const { data, error } = useAsyncFunction(fetchAllGames)
// ...
}
SWR 具有类似的 API,因此让我们从这个钩子开始,并根据需要进行更改。
更改数据存储
我们可以将数据存储React.useState
在模块范围内的静态变量中,而不是存储数据,然后我们可以data
从状态中删除该属性:
const cache: Map<string, unknown> = new Map()
type State<T> = { error?: string }
我们的缓存是Map
因为否则钩子的不同消费者会用他们不相关的数据覆盖缓存。
key
这意味着我们需要向钩子添加一个参数:
export function useAsyncFunction<T>(key: string, asyncFunction: () => Promise<T>) {
...
}
接下来,我们改变承诺解决时发生的事情:
asyncFunction()
.then(data => {
cache.set(key, data) // <<<<<<<<<<<<< setting cache here!
setState({ error: undefined })
})
.catch(error => {
setState({ error: error.toString() })
})
现在我们的“状态”只是错误,所以我们可以简化它。自定义钩子现在如下所示:
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 }
}
改变本地数据
这是可行的,但它没有提供改变本地数据或重新加载它的机制。
我们可以创建一个“mutate”方法来更新缓存中的数据,并通过将其添加到返回对象来暴露它。我们希望记住它,这样函数引用就不会在每次渲染时发生变化。(React 文档中关于 useCallback 的内容):
...
const mutate = React.useCallback(
(data: T) => void cache.set(key, data),
[key]
);
return { data, error, mutate };
}
接下来,为了提供“重新加载”功能,我们提取当前位于我们useEffect
的匿名函数内的现有“加载”实现:
React.useEffect(() => {
asyncFunction()
.then(data => {
cache.set(key, data)
setError(undefined)
})
.catch(error => setError(error.toString()))
}, [key, asyncFunction])
再次,我们需要将函数包装在其中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 };
快到了
整个模块现在看起来像这样:(⚠️但它不起作用)
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 }
}
⚠️ 上面的代码不起作用,因为第一次执行时,data
是 undefined。之后,promise 解析成功,cache
被更新,但由于我们没有使用useState
,React 不会重新渲染组件。
厚颜无耻地强制更新
这是一个强制更新我们组件的快速钩子。
function useForceUpdate() {
const [, setState] = React.useState<number[]>([])
return React.useCallback(() => setState([]), [setState])
}
我们这样使用它:
...
const forceUpdate = useForceUpdate();
const mutate = React.useCallback(
(data: T) => {
cache.set(key, data);
forceUpdate(); // <<<<<<< calling forceUpdate after setting the cache!
},
[key, forceUpdate]
);
...
✅现在一切正常!当 Promise 解析成功并设置缓存后,组件会被强制更新,并最终data
指向缓存中的值。
const data = cache.get(key) as T | undefined
return { data, error, mutate, reload: load }
通知其他组件
这可行,但还不够好。
当多个 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)!;
}
请注意,我们不再cache
直接导出对象。取而代之的是getCache
和setCache
函数。但更重要的是,我们还导出了subscribe
和unsubscribe
函数。这些函数用于让我们的组件订阅更改,即使这些更改不是由它们发起的。
让我们更新自定义钩子来使用这些函数。首先:
-cache.set(key, data);
+setCache(key, data);
...
-const data = cache.get(key) as T | undefined;
+const data = getCache(key) as T | undefined;
然后,为了订阅更改,我们需要一个新的useEffect
:
React.useEffect(() =>{
subscribe(key, forceUpdate);
return () => unsubscribe(key, forceUpdate)
}, [key, forceUpdate])
这里我们在组件挂载时订阅特定键的缓存,并unsubscribe
在组件卸载时(或者 props 发生变化时)在返回的清理函数中订阅缓存。(React 文档中关于 useEffect 的内容)
我们可以稍微整理一下我们的mutate
函数。我们不需要forceUpdate
从中调用它,因为它现在作为setCache
订阅的结果被调用:
const mutate = React.useCallback(
(data: T) => {
setCache(key, data);
- forceUpdate();
},
- [key, forceUpdate]
+ [key]
);
最终版本
我们的自定义钩子现在看起来像这样:
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]);
}
此实现不适用于生产环境。它与 SWR 的功能基本相似,但缺少该库的许多优秀功能。
✅ 包含 | ❌ 不包括 |
---|---|
获取时返回缓存值 | 删除相同的请求 |
提供(重新验证)重新加载功能 | 焦点重新验证 |
局部突变 | 按间隔重新获取 |
滚动位置恢复和分页 | |
依赖抓取 | |
悬念 |
结论
我认为SWR(或react-queryuseState
)是比使用或将获取的数据存储在 React 组件中更好的解决方案useReducer
。
我继续使用自定义钩子来存储我的应用程序状态useReducer
,但useState
对于远程数据,我更喜欢将其存储在缓存中。
文章来源:https://dev.to/juliang/how-swr-works-4lkb