cache
允许您缓存数据获取或计算的结果。
const cachedFn = cache(fn);
参考
cache(fn)
在任何组件外部调用 cache
以创建一个具有缓存功能的函数版本。
import {cache} from 'react';
import calculateMetrics from 'lib/metrics';
const getMetrics = cache(calculateMetrics);
function Chart({data}) {
const report = getMetrics(data);
// ...
}
当 getMetrics
首次使用 data
调用时,getMetrics
将调用 calculateMetrics(data)
并将结果存储在缓存中。如果再次使用相同的 data
调用 getMetrics
,它将返回缓存的结果,而不是再次调用 calculateMetrics(data)
。
参数
fn
:您要为其缓存结果的函数。fn
可以接受任何参数并返回任何值。
返回值
cache
返回具有相同类型签名的 fn
的缓存版本。在此过程中,它不会调用 fn
。
当使用给定参数调用 cachedFn
时,它首先检查缓存中是否存在缓存的结果。如果存在缓存的结果,则返回该结果。如果没有,它将使用参数调用 fn
,将结果存储在缓存中,并返回该结果。仅当缓存未命中时才会调用 fn
。
注意事项
- React 会在每次服务器请求时使所有记忆函数的缓存失效。
- 每次调用
cache
都会创建一个新函数。这意味着多次使用相同函数调用cache
将返回不同的记忆函数,这些函数不共享相同的缓存。 cachedFn
也会缓存错误。如果fn
对某些参数抛出错误,则该错误将被缓存,并且当使用相同的参数调用cachedFn
时,将重新抛出相同的错误。cache
仅用于 服务器组件。
用法
缓存耗时的计算
使用 cache
来跳过重复工作。
import {cache} from 'react';
import calculateUserMetrics from 'lib/user';
const getUserMetrics = cache(calculateUserMetrics);
function Profile({user}) {
const metrics = getUserMetrics(user);
// ...
}
function TeamReport({users}) {
for (let user in users) {
const metrics = getUserMetrics(user);
// ...
}
// ...
}
如果相同的 user
对象在 Profile
和 TeamReport
中都进行了渲染,则这两个组件可以共享工作,并且只为该 user
调用一次 calculateUserMetrics
。
假设首先渲染 Profile
。它将调用 getUserMetrics
,并检查是否存在缓存结果。由于这是第一次使用该 user
调用 getUserMetrics
,因此会出现缓存未命中。getUserMetrics
随后将使用该 user
调用 calculateUserMetrics
,并将结果写入缓存。
当 TeamReport
渲染其 users
列表并到达相同的 user
对象时,它将调用 getUserMetrics
并从缓存中读取结果。
共享数据快照
要在组件之间共享数据快照,请使用数据获取函数(如 fetch
)调用 cache
。当多个组件进行相同的数据获取时,只发出一个请求,并且返回的数据会被缓存并在组件之间共享。所有组件都引用服务器渲染中相同的数据快照。
import {cache} from 'react';
import {fetchTemperature} from './api.js';
const getTemperature = cache(async (city) => {
return await fetchTemperature(city);
});
async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}
async function MinimalWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}
如果 AnimatedWeatherCard
和 MinimalWeatherCard
都为相同的 城市 进行渲染,则它们将从 记忆函数 中接收相同的数据快照。
如果 AnimatedWeatherCard
和 MinimalWeatherCard
向 getTemperature
提供不同的 城市 参数,则 fetchTemperature
将被调用两次,并且每个调用位置将收到不同的数据。
城市 充当缓存键。
预加载数据
通过缓存长时间运行的数据获取,您可以在渲染组件之前启动异步工作。
const getUser = cache(async (id) => {
return await db.user.query(id);
})
async function Profile({id}) {
const user = await getUser(id);
return (
<section>
<img src={user.profilePic} />
<h2>{user.name}</h2>
</section>
);
}
function Page({id}) {
// ✅ Good: start fetching the user data
getUser(id);
// ... some computational work
return (
<>
<Profile id={id} />
</>
);
}
在渲染 Page
时,组件会调用 getUser
,但请注意,它不使用返回的数据。这个早期的 getUser
调用会在 Page
执行其他计算工作和渲染子组件时启动异步数据库查询。
在渲染 Profile
时,我们再次调用 getUser
。如果初始的 getUser
调用已经返回并缓存了用户数据,那么当 Profile
请求并等待此数据 时,它可以直接从缓存中读取,而无需再次进行远程过程调用。如果 初始数据请求 尚未完成,则以这种模式预加载数据可以减少数据获取的延迟。
深入探讨
当评估一个异步函数时,您将收到一个表示该工作的Promise。Promise 包含该工作的状态(*pending*、*fulfilled*、*failed*)及其最终的解决结果。
在本例中,异步函数 fetchData
返回一个正在等待 fetch
的 Promise。
async function fetchData() {
return await fetch(`https://...`);
}
const getData = cache(fetchData);
async function MyComponent() {
getData();
// ... some computational work
await getData();
// ...
}
在第一次调用 getData
时,从 fetchData
返回的 Promise 被缓存。后续的查找将返回相同的 Promise。
请注意,第一次 getData
调用没有 await
,而第二次调用了。await
是一个 JavaScript 运算符,它将等待并返回 Promise 的解决结果。第一次 getData
调用只是简单地启动 fetch
来缓存 Promise,以便第二次 getData
进行查找。
如果到 第二次调用 时,Promise 仍处于 *pending* 状态,则 await
将暂停并等待结果。优化之处在于,在我们等待 fetch
的同时,React 可以继续执行计算工作,从而减少了 第二次调用 的等待时间。
如果 Promise 已经解决,无论是出现错误还是 *fulfilled* 结果,await
都会立即返回该值。在这两种结果中,性能都会有所提高。
深入探讨
所有提到的 API 都提供记忆功能,但区别在于它们的记忆对象、谁可以访问缓存以及缓存何时失效。
useMemo
通常,您应该使用 useMemo
在客户端组件的多次渲染之间缓存计算量大的结果。例如,记忆组件内的数据转换。
'use client';
function WeatherReport({record}) {
const avgTemp = useMemo(() => calculateAvg(record)), record);
// ...
}
function App() {
const record = getRecord();
return (
<>
<WeatherReport record={record} />
<WeatherReport record={record} />
</>
);
}
在此示例中,App
使用相同的记录渲染了两个 WeatherReport
。即使两个组件执行相同的工作,它们也不能共享工作成果。useMemo
的缓存仅对组件本地有效。
但是,useMemo
确实可以确保如果 App
重新渲染并且 record
对象没有更改,则每个组件实例都会跳过工作并使用 avgTemp
的记忆值。useMemo
只会缓存具有给定依赖项的 avgTemp
的最后一次计算结果。
cache
通常,您应该在服务器组件中使用 cache
来记忆可以在组件之间共享的工作成果。
const cachedFetchReport = cache(fetchReport);
function WeatherReport({city}) {
const report = cachedFetchReport(city);
// ...
}
function App() {
const city = "Los Angeles";
return (
<>
<WeatherReport city={city} />
<WeatherReport city={city} />
</>
);
}
将前面的示例重写为使用 cache
,在本例中,WeatherReport
的第二个实例 将能够跳过重复工作并从与 WeatherReport
的第一个实例相同的缓存中读取数据。与前一个示例的另一个区别是,cache
也被推荐用于 记忆数据获取,这与 useMemo
不同,后者应该仅用于计算。
目前,cache
应该只在服务器组件中使用,并且缓存将在服务器请求之间失效。
memo
如果组件的 props 没有变化,您应该使用 memo
来防止组件重新渲染。
'use client';
function WeatherReport({record}) {
const avgTemp = calculateAvg(record);
// ...
}
const MemoWeatherReport = memo(WeatherReport);
function App() {
const record = getRecord();
return (
<>
<MemoWeatherReport record={record} />
<MemoWeatherReport record={record} />
</>
);
}
在此示例中,两个 MemoWeatherReport
组件在首次渲染时都会调用 calculateAvg
。但是,如果 App
重新渲染,并且 record
没有变化,则没有任何 props 发生变化,MemoWeatherReport
将不会重新渲染。
与 useMemo
相比,memo
根据 props 而不是特定的计算结果来记忆组件渲染。与 useMemo
类似,记忆组件只缓存具有最后一次 prop 值的最后一次渲染。一旦 props 发生变化,缓存就会失效,组件就会重新渲染。
故障排除
即使我使用相同的参数调用了记忆函数,但它仍然在运行
请参阅前面提到的陷阱
如果以上情况均不适用,则可能是 React 检查缓存中是否存在内容的方式存在问题。
如果您的参数不是 基本类型(例如对象、函数、数组),请确保您传递的是相同的对象引用。
调用记忆函数时,React 会查找输入参数以查看结果是否已缓存。React 将使用参数的浅层相等性来确定是否存在缓存命中。
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// 🚩 Wrong: props is an object that changes every render.
const length = calculateNorm(props);
// ...
}
function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}
在这种情况下,这两个 MapMarker
看起来像是做了同样的工作,并用相同的值 {x: 10, y: 10, z:10}
调用 calculateNorm
。即使这些对象包含相同的值,但它们不是相同的对象引用,因为每个组件都创建了自己的 props
对象。
React 会调用输入上的 Object.is
来验证是否存在缓存命中。
import {cache} from 'react';
const calculateNorm = cache((x, y, z) => {
// ...
});
function MapMarker(props) {
// ✅ Good: Pass primitives to memoized function
const length = calculateNorm(props.x, props.y, props.z);
// ...
}
function App() {
return (
<>
<MapMarker x={10} y={10} z={10} />
<MapMarker x={10} y={10} z={10} />
</>
);
}
解决此问题的一种方法是将向量维度传递给 calculateNorm
。这种方法可行,因为维度本身是原始值。
另一个解决方案可能是将向量对象本身作为 prop 传递给组件。我们需要将相同的对象传递给两个组件实例。
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// ✅ Good: Pass the same `vector` object
const length = calculateNorm(props.vector);
// ...
}
function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}