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} />
</>
);
}