cache - This feature is available in the latest Canary

Canary

cache 允许你缓存数据获取或计算的结果。

¥cache lets you cache the result of a data fetch or computation.

const cachedFn = cache(fn);

参考

¥Reference

cache(fn)

在任何组件外部调用 cache 以创建具有缓存的函数版本。

¥Call cache outside of any components to create a version of the function with caching.

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)

¥When getMetrics is first called with data, getMetrics will call calculateMetrics(data) and store the result in cache. If getMetrics is called again with the same data, it will return the cached result instead of calling calculateMetrics(data) again.

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • fn:你想要缓存结果的函数。fn 可以接受任何参数并返回任何值。

    ¥fn: The function you want to cache results for. fn can take any arguments and return any value.

返回

¥Returns

cache 返回具有相同类型签名的 fn 的缓存版本。在此过程中它不会调用 fn

¥cache returns a cached version of fn with the same type signature. It does not call fn in the process.

当使用给定参数调用 cachedFn 时,它首先检查缓存中是否存在缓存结果。如果存在缓存结果,则返回结果。如果没有,则使用参数调用 fn,将结果存储在缓存中,然后返回结果。唯一一次调用 fn 是当缓存未命中时。

¥When calling cachedFn with given arguments, it first checks if a cached result exists in the cache. If a cached result exists, it returns the result. If not, it calls fn with the arguments, stores the result in the cache, and returns the result. The only time fn is called is when there is a cache miss.

注意

基于输入的缓存返回值的优化被称为 memoization。我们将从 cache 返回的函数称为记忆函数。

¥The optimization of caching return values based on inputs is known as memoization. We refer to the function returned from cache as a memoized function.

注意事项

¥Caveats

  • React 将使每个服务器请求的所有记忆函数的缓存失效。

    ¥React will invalidate the cache for all memoized functions for each server request.

  • 每次调用 cache 都会创建一个新函数。这意味着多次使用同一函数调用 cache 将返回不共享相同缓存的不同记忆函数。

    ¥Each call to cache creates a new function. This means that calling cache with the same function multiple times will return different memoized functions that do not share the same cache.

  • cachedFn 也会缓存错误。如果 fn 对某些参数抛出错误,它将被缓存,并且当使用这些相同的参数调用 cachedFn 时,会重新抛出相同的错误。

    ¥cachedFn will also cache errors. If fn throws an error for certain arguments, it will be cached, and the same error is re-thrown when cachedFn is called with those same arguments.

  • cache 仅适用于 服务器组件

    ¥cache is for use in Server Components only.


用法

¥Usage

缓存昂贵的计算

¥Cache an expensive computation

使用 cache 跳过重复的工作。

¥Use cache to skip duplicate work.

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

如果在 ProfileTeamReport 中渲染相同的 user 对象,则两个组件可以共享工作,并且只为 user 调用 calculateUserMetrics 一次。

¥If the same user object is rendered in both Profile and TeamReport, the two components can share work and only call calculateUserMetrics once for that user.

假设首先渲染 Profile。它会调用getUserMetrics,并检查是否有缓存的结果。由于这是第一次使用 user 调用 getUserMetrics,因此会出现缓存未命中。然后,getUserMetrics 将使用 user 调用 calculateUserMetrics,并将结果写入缓存。

¥Assume Profile is rendered first. It will call getUserMetrics, and check if there is a cached result. Since it is the first time getUserMetrics is called with that user, there will be a cache miss. getUserMetrics will then call calculateUserMetrics with that user and write the result to cache.

TeamReport 渲染其 users 列表并到达相同的 user 对象时,它将调用 getUserMetrics 并从缓存中读取结果。

¥When TeamReport renders its list of users and reaches the same user object, it will call getUserMetrics and read the result from cache.

易犯错误

调用不同的记忆函数将从不同的缓存中读取。

¥Calling different memoized functions will read from different caches.

要访问相同的缓存,组件必须调用相同的记忆函数。

¥To access the same cache, components must call the same memoized function.

// Temperature.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export function Temperature({cityData}) {
// 🚩 Wrong: Calling `cache` in component creates new `getWeekReport` for each render
const getWeekReport = cache(calculateWeekReport);
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

// 🚩 Wrong: `getWeekReport` is only accessible for `Precipitation` component.
const getWeekReport = cache(calculateWeekReport);

export function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

在上面的示例中, PrecipitationTemperature 各自调用 cache 以使用自己的缓存查找创建一个新的记忆函数。如果两个组件都渲染相同的 cityData,它们将执行重复的工作来调用 calculateWeekReport

¥In the above example, Precipitation and Temperature each call cache to create a new memoized function with their own cache look-up. If both components render for the same cityData, they will do duplicate work to call calculateWeekReport.

此外,Temperature 在每次渲染组件时都会创建一个新的记忆函数,这不允许任何缓存共享。

¥In addition, Temperature creates a new memoized function each time the component is rendered which doesn’t allow for any cache sharing.

为了最大限度地提高缓存命中率并减少工作量,两个组件应调用相同的记忆函数来访问相同的缓存。相反,在专用模块中定义记忆函数,该模块可以跨组件 import-ed

¥To maximize cache hits and reduce work, the two components should call the same memoized function to access the same cache. Instead, define the memoized function in a dedicated module that can be import-ed across components.

// getWeekReport.js
import {cache} from 'react';
import {calculateWeekReport} from './report';

export default cache(calculateWeekReport);
// Temperature.js
import getWeekReport from './getWeekReport';

export default function Temperature({cityData}) {
const report = getWeekReport(cityData);
// ...
}
// Precipitation.js
import getWeekReport from './getWeekReport';

export default function Precipitation({cityData}) {
const report = getWeekReport(cityData);
// ...
}

这里,两个组件都调用从 ./getWeekReport.js 导出的相同的记忆函数来读取和写入相同的缓存。

¥Here, both components call the same memoized function exported from ./getWeekReport.js to read and write to the same cache.

分享数据快照

¥Share a snapshot of data

要在组件之间共享数据快照,请使用 fetch 等数据获取函数调用 cache。当多个组件进行相同的数据获取时,仅发出一个请求,并且返回的数据被缓存并在组件之间共享。所有组件都引用服务器渲染中的相同数据快照。

¥To share a snapshot of data between components, call cache with a data-fetching function like fetch. When multiple components make the same data fetch, only one request is made and the data returned is cached and shared across components. All components refer to the same snapshot of data across the server render.

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

如果 AnimatedWeatherCardMinimalWeatherCard 都针对同一城市进行渲染,则它们将从记忆函数接收相同的数据快照。

¥If AnimatedWeatherCard and MinimalWeatherCard both render for the same city, they will receive the same snapshot of data from the memoized function.

如果 AnimatedWeatherCardMinimalWeatherCardgetTemperature 提供不同的 city 参数,则 fetchTemperature 将被调用两次,每个调用站点将收到不同的数据。

¥If AnimatedWeatherCard and MinimalWeatherCard supply different city arguments to getTemperature, then fetchTemperature will be called twice and each call site will receive different data.

city 充当缓存键。

¥The city acts as a cache key.

注意

异步渲染仅支持服务器组件。

¥Asynchronous rendering is only supported for Server Components.

async function AnimatedWeatherCard({city}) {
const temperature = await getTemperature(city);
// ...
}

预加载数据

¥Preload data

通过缓存长时间运行的数据获取,你可以在渲染组件之前启动异步工作。

¥By caching a long-running data fetch, you can kick off asynchronous work prior to rendering the component.

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 执行其他计算工作和渲染子项时发生的异步数据库查询。

¥When rendering Page, the component calls getUser but note that it doesn’t use the returned data. This early getUser call kicks off the asynchronous database query that occurs while Page is doing other computational work and rendering children.

当渲染 Profile 时,我们再次调用getUser。如果初始getUser调用已经返回并缓存了用户数据,那么当 Profile请求并等待该数据时,只需从缓存中读取即可 不需要另一个远程过程调用。如果初始数据请求尚未完成,则以此模式预加载数据可以减少数据获取的延迟。

¥When rendering Profile, we call getUser again. If the initial getUser call has already returned and cached the user data, when Profile asks and waits for this data, it can simply read from the cache without requiring another remote procedure call. If the initial data request hasn’t been completed, preloading data in this pattern reduces delay in data-fetching.

深入研究

缓存异步工作

¥Caching asynchronous work

当评估 异步函数 时,你将收到该工作的 Promise 分。Promise 保存了该工作的状态(待处理、已完成、失败)及其最终确定的结果。

¥When evaluating an asynchronous function, you will receive a Promise for that work. The promise holds the state of that work (pending, fulfilled, failed) and its eventual settled result.

在此示例中,异步函数 fetchData 返回等待 fetch 的 Promise。

¥In this example, the asynchronous function fetchData returns a promise that is awaiting the fetch.

async function fetchData() {
return await fetch(`https://...`);
}

const getData = cache(fetchData);

async function MyComponent() {
getData();
// ... some computational work
await getData();
// ...
}

第一次调用 getData 时,fetchData 返回的 Promise 会被缓存。随后的查找将返回相同的 promise。

¥In calling getData the first time, the promise returned from fetchData is cached. Subsequent look-ups will then return the same promise.

请注意,第一个 getData 调用不会执行 await,而 第二个 则会执行。await 是一个 JavaScript 运算符,它将等待并返回 Promise 的确定结果。第一个 getData 调用只是启动 fetch 来缓存第二个 getData 查找的 promise。

¥Notice that the first getData call does not await whereas the second does. await is a JavaScript operator that will wait and return the settled result of the promise. The first getData call simply initiates the fetch to cache the promise for the second getData to look-up.

如果到第二次调用,promise 仍然处于待处理状态,那么 await 将暂停等待结果。优化之处在于,当我们等待 fetch 时,React 可以继续进行计算工作,从而减少了第二次调用的等待时间。

¥If by the second call the promise is still pending, then await will pause for the result. The optimization is that while we wait on the fetch, React can continue with computational work, thus reducing the wait time for the second call.

如果 promise 已经解决,无论是错误还是已实现的结果,await 将立即返回该值。这两种结果都具有性能优势。

¥If the promise is already settled, either to an error or the fulfilled result, await will return that value immediately. In both outcomes, there is a performance benefit.

易犯错误

在组件外部调用记忆函数将不会使用缓存。

¥Calling a memoized function outside of a component will not use the cache.

import {cache} from 'react';

const getUser = cache(async (userId) => {
return await db.user.query(userId);
});

// 🚩 Wrong: Calling memoized function outside of component will not memoize.
getUser('demo-id');

async function DemoProfile() {
// ✅ Good: `getUser` will memoize.
const user = await getUser('demo-id');
return <Profile user={user} />;
}

React 仅提供对组件中记忆函数的缓存访问。当在组件外部调用 getUser 时,它仍然会计算该函数,但不会读取或更新缓存。

¥React only provides cache access to the memoized function in a component. When calling getUser outside of a component, it will still evaluate the function but not read or update the cache.

这是因为缓存访问是通过 上下文 提供的,而 上下文 只能从组件访问。

¥This is because cache access is provided through a context which is only accessible from a component.

深入研究

我什么时候应该使用 cachememouseMemo

¥When should I use cache, memo, or useMemo?

所有提到的 API 都提供记忆功能,但区别在于它们要记忆的内容、谁可以访问缓存以及缓存何时失效。

¥All mentioned APIs offer memoization but the difference is what they’re intended to memoize, who can access the cache, and when their cache is invalidated.

useMemo

一般来说,你应该使用 useMemo 在跨渲染的客户端组件中缓存昂贵的计算。例如,记住组件内数据的转换。

¥In general, you should use useMemo for caching a expensive computation in a Client Component across renders. As an example, to memoize a transformation of data within a component.

'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 的缓存仅位于组件本地。

¥In this example, App renders two WeatherReports with the same record. Even though both components do the same work, they cannot share work. useMemo’s cache is only local to the component.

但是,useMemo 确实确保如果 App 重新渲染并且 record 对象没有更改,则每个组件实例将跳过工作并使用 avgTemp 的记忆值。useMemo 将仅缓存具有给定依赖的 avgTemp 的最后计算。

¥However, useMemo does ensure that if App re-renders and the record object doesn’t change, each component instance would skip work and use the memoized value of avgTemp. useMemo will only cache the last computation of avgTemp with the given dependencies.

cache

一般来说,你应该在服务器组件中使用 cache 来记住可以跨组件共享的工作。

¥In general, you should use cache in Server Components to memoize work that can be shared across components.

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 不同,useMemo 只能用于计算。

¥Re-writing the previous example to use cache, in this case the second instance of WeatherReport will be able to skip duplicate work and read from the same cache as the first WeatherReport. Another difference from the previous example is that cache is also recommended for memoizing data fetches, unlike useMemo which should only be used for computations.

此时,cache 应该仅在 Server Components 中使用,并且缓存将在跨服务器请求时失效。

¥At this time, cache should only be used in Server Components and the cache will be invalidated across server requests.

memo

如果组件的属性未更改,你应该使用 memo 来防止组件重新渲染。

¥You should use memo to prevent a component re-rendering if its props are unchanged.

'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 没有变化,则所有属性都不会改变,MemoWeatherReport 也不会重新渲染。

¥In this example, both MemoWeatherReport components will call calculateAvg when first rendered. However, if App re-renders, with no changes to record, none of the props have changed and MemoWeatherReport will not re-render.

useMemo 相比,memo 根据属性与特定计算来记忆组件渲染。与 useMemo 类似,记忆组件仅缓存最后一次渲染和最后一个属性值。一旦属性改变,缓存就会失效,组件会重新渲染。

¥Compared to useMemo, memo memoizes the component render based on props vs. specific computations. Similar to useMemo, the memoized component only caches the last render with the last prop values. Once the props change, the cache invalidates and the component re-renders.


故障排除

¥Troubleshooting

即使我使用相同的参数调用了我的记忆函数,它仍然会运行

¥My memoized function still runs even though I’ve called it with the same arguments

请参阅前面提到的陷阱

¥See prior mentioned pitfalls

如果以上情况都不适用,则可能是 React 如何检查缓存中是否存在某些内容存在问题。

¥If none of the above apply, it may be a problem with how React checks if something exists in cache.

如果你的参数不是 primitives(例如对象、函数、数组),请确保你传递相同的对象引用。

¥If your arguments are not primitives (ex. objects, functions, arrays), ensure you’re passing the same object reference.

当调用记忆函数时,React 将查找输入参数以查看结果是否已缓存。React 将使用参数的浅相等来确定是否存在缓存命中。

¥When calling a memoized function, React will look up the input arguments to see if a result is already cached. React will use shallow equality of the arguments to determine if there is a cache hit.

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 对象。

¥In this case the two MapMarkers look like they’re doing the same work and calling calculateNorm with the same value of {x: 10, y: 10, z:10}. Even though the objects contain the same values, they are not the same object reference as each component creates its own props object.

React 将在输入上调用 Object.is 以验证是否存在缓存命中。

¥React will call Object.is on the input to verify if there is a cache hit.

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。这是可行的,因为维度本身就是基础类型。

¥One way to address this could be to pass the vector dimensions to calculateNorm. This works because the dimensions themselves are primitives.

另一种解决方案可能是将矢量对象本身作为组件传递给组件。我们需要将相同的对象传递给两个组件实例。

¥Another solution may be to pass the vector object itself as a prop to the component. We’ll need to pass the same object to both component instances.

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

React 中文网 - 粤ICP备13048890号