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);
// ...
}当首次使用 data 调用 getMetrics 时,getMetrics 会调用 calculateMetrics(data),并将结果存入缓存。如果再次使用相同的 data 调用 getMetrics,它会返回缓存结果,而不是再次调用 calculateMetrics(data)。
参数
fn:你希望缓存结果的函数。fn可以接受任意参数并返回任意值。
返回值
cache 返回一个与 fn 具有相同类型签名的缓存版本。这个过程不会调用 fn。
当使用给定参数调用 cachedFn 时,它首先检查缓存中是否存在已缓存结果。如果存在,则返回该结果;如果不存在,则使用这些参数调用 fn,将结果存入缓存,并返回结果。只有在缓存未命中时才会调用 fn。
注意事项
- React 会在每次服务器请求时使所有记忆化函数的缓存失效。
- 每次调用
cache都会创建一个新函数。这意味着用同一个函数多次调用cache会返回不同的记忆化函数,它们不共享同一缓存。 cachedFn也会缓存错误。如果fn对某些参数抛出错误,该错误会被缓存,并且当使用相同参数调用cachedFn时会重新抛出相同错误。cache仅用于 Server Components。
用法
缓存昂贵的计算
使用 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,并从缓存中读取结果。
如果 calculateUserMetrics 可以通过传入 AbortSignal 中止,你可以使用 cacheSignal() 在 React 完成渲染时取消这项昂贵计算。calculateUserMetrics 也可能已经通过直接使用 cacheSignal 在内部处理了取消。
共享数据快照
要在组件之间共享数据快照,可以将 cache 与类似 fetch 的数据获取函数一起调用。当多个组件进行相同的数据请求时,只会发出一次请求,返回的数据会被缓存并在组件之间共享。所有组件在整个服务器渲染过程中引用同一个数据快照。
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 都为同一个 city 渲染,它们将从 记忆化函数 中获得相同的数据快照。
如果 AnimatedWeatherCard 和 MinimalWeatherCard 向 getTemperature 提供不同的 city 参数,那么 fetchTemperature 会被调用两次,并且每个调用点都会收到不同的数据。
city 充当缓存键。
预加载数据
通过缓存一个耗时的数据获取,你可以在渲染组件之前就开始异步工作。
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}) {
// ✅ 好:开始获取用户数据
getUser(id);
// ... 一些计算工作
return (
<>
<Profile id={id} />
</>
);
}渲染 Page 时,组件会调用 getUser,但请注意它并未使用返回的数据。这个提前的 getUser 调用会启动异步数据库查询,而此时 Page 正在执行其他计算工作并渲染子组件。
渲染 Profile 时,我们再次调用 getUser。如果最初的 getUser 调用已经返回并缓存了用户数据,那么当 Profile 请求并等待这些数据 时,它可以直接从缓存中读取,而无需再次进行远程过程调用。如果 最初的数据请求 尚未完成,这种预加载模式可以减少数据获取延迟。
Deep Dive
在计算一个 异步函数 时,你会得到该工作的一个 Promise。promise 持有这项工作的状态(pending、fulfilled、failed)以及其最终完成结果。
在这个示例中,异步函数 fetchData 返回一个正在等待 fetch 的 promise。
async function fetchData() {
return await fetch(`https://...`);
}
const getData = cache(fetchData);
async function MyComponent() {
getData();
// ... 一些计算工作
await getData();
// ...
}在第一次调用 getData 时,来自 fetchData 的 promise 会被缓存。后续查找将返回同一个 promise。
注意,第一次 getData 调用没有使用 await,而 第二次 使用了。await 是一个 JavaScript 运算符,它会等待并返回 promise 的最终结果。第一次 getData 调用只是启动 fetch,以缓存该 promise,供第二次 getData 查找。
如果到 第二次调用 时该 promise 仍然处于 pending,那么 await 会暂停等待结果。优化之处在于:当我们等待 fetch 时,React 可以继续执行计算工作,从而减少 第二次调用 的等待时间。
如果该 promise 已经完成,无论是错误还是 fulfilled 结果,await 都会立即返回该值。在这两种情况下,性能都会受益。
Deep Dive
上述 API 都提供记忆化功能,但区别在于它们各自 intended to memoize 的内容、谁可以访问缓存,以及缓存何时失效。
useMemo
通常,你应该在 Client Component 中使用 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 使用相同的 record 渲染了两个 WeatherReport。尽管两个组件做的是相同的工作,它们也不能共享工作。useMemo 的缓存只局限于组件本身。
不过,useMemo 确实可以保证:如果 App 重新渲染且 record 对象没有变化,每个组件实例都会跳过工作并使用 avgTemp 的记忆化值。useMemo 只会缓存给定依赖下 avgTemp 的最后一次计算。
cache
通常,你应该在 Server Components 中使用 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 只能用于 Server Components,并且缓存会在不同服务器请求之间失效。
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 类似,被记忆化的组件只会缓存上一次 props 值对应的最后一次渲染。一旦 props 改变,缓存就会失效,组件会重新渲染。
故障排查
即使我用相同的参数调用了它,我的记忆化函数还是会运行
请参见前面提到的陷阱
如果以上都不适用,可能是 React 检查缓存中是否存在某项内容的方式有问题。
如果你的参数不是 原始类型(例如对象、函数、数组),请确保你传入的是同一个对象引用。
在调用记忆化函数时,React 会查找输入参数,以查看结果是否已经被缓存。React 会使用参数的浅比较来判断是否命中缓存。
import {cache} from 'react';
const calculateNorm = cache((vector) => {
// ...
});
function MapMarker(props) {
// 🚩 错误:props 是一个每次渲染都会变化的对象。
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) {
// ✅ 好:向记忆化函数传递原始类型
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) {
// ✅ 好:传递相同的 `vector` 对象
const length = calculateNorm(props.vector);
// ...
}
function App() {
const vector = [10, 10, 10];
return (
<>
<MapMarker vector={vector} />
<MapMarker vector={vector} />
</>
);
}