memo 允许你在属性不变时跳过重新渲染组件。

¥memo lets you skip re-rendering a component when its props are unchanged.

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

参考

¥Reference

memo(Component, arePropsEqual?)

memo 中封装一个组件以获得该组件的记忆化版本。只要组件的属性 没有改变,当它的父组件被重新渲染时,你的组件的这个记忆化版本通常不会被重新渲染。但 React 可能仍会重新渲染它:记忆化是一种性能优化,而不是保证。

¥Wrap a component in memo to get a memoized version of that component. This memoized version of your component will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed. But React may still re-render it: memoization is a performance optimization, not a guarantee.

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
// ...
});

请参阅下面的更多示例。

¥See more examples below.

参数

¥Parameters

  • Component:你要记忆化的组件。memo 不修改此组件,而是返回一个新的记忆化组件。接受任何有效的 React 组件,包括函数和 forwardRef 组件。

    ¥Component: The component that you want to memoize. The memo does not modify this component, but returns a new, memoized component instead. Any valid React component, including functions and forwardRef components, is accepted.

  • 可选 arePropsEqual:接受两个参数的函数:该组件的先前属性及其新属性。如果新旧属性相等,它应该返回 true:也就是说,如果组件将渲染相同的输出并以与旧属性相同的方式运行新属性。否则它应该返回 false。通常,你不会指定此功能。默认情况下,React 会将每个属性与 Object.is. 进行比较

    ¥optional arePropsEqual: A function that accepts two arguments: the component’s previous props, and its new props. It should return true if the old and new props are equal: that is, if the component will render the same output and behave in the same way with the new props as with the old. Otherwise it should return false. Usually, you will not specify this function. By default, React will compare each prop with Object.is.

返回

¥Returns

memo 返回一个新的 React 组件。它的行为与提供给 memo 的组件相同,除了 React 不会总是在其父项被重新渲染时重新渲染它,除非它的属性 发生了变化。

¥memo returns a new React component. It behaves the same as the component provided to memo except that React will not always re-render it when its parent is being re-rendered unless its props have changed.


用法

¥Usage

属性不变时跳过重新渲染

¥Skipping re-rendering when props are unchanged

React 通常会在父级重新渲染时重新渲染组件。使用 memo,你可以创建一个组件,只要其新属性与旧属性相同,React 就不会在其父级重新渲染时重新渲染。这样的组件被称为记忆化的。

¥React normally re-renders a component whenever its parent re-renders. With memo, you can create a component that React will not re-render when its parent re-renders so long as its new props are the same as the old props. Such a component is said to be memoized.

要记忆化组件,请将其封装在 memo 中并使用它返回的值代替原始组件:

¥To memoize a component, wrap it in memo and use the value that it returns in place of your original component:

const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

React 组件应该总是有 纯渲染逻辑。 这意味着如果它的属性、状态和上下文没有改变,它必须返回相同的输出。通过使用 memo,你告诉 React 你的组件符合这个要求,所以只要它的属性 没有改变,React 就不需要重新渲染。即使使用 memo,如果组件自身的状态发生变化或者它使用的上下文发生变化,你的组件也会重新渲染。

¥A React component should always have pure rendering logic. This means that it must return the same output if its props, state, and context haven’t changed. By using memo, you are telling React that your component complies with this requirement, so React doesn’t need to re-render as long as its props haven’t changed. Even with memo, your component will re-render if its own state changes or if a context that it’s using changes.

在此示例中,请注意 Greeting 组件会在 name 发生更改时重新渲染(因为这是它的属性之一),但不会在 address 更改时重新渲染(因为它没有作为属性传递给 Greeting):

¥In this example, notice that the Greeting component re-renders whenever name is changed (because that’s one of its props), but not when address is changed (because it’s not passed to Greeting as a prop):

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  return <h3>Hello{name && ', '}{name}!</h3>;
});

注意

你应该只依赖 memo 作为性能优化。如果你的代码没有它就不能工作,找到潜在的问题并首先修复它。然后你可以添加 memo 来提高性能。

¥You should only rely on memo as a performance optimization. If your code doesn’t work without it, find the underlying problem and fix it first. Then you may add memo to improve performance.

深入研究

你应该到处添加备忘录吗?

¥Should you add memo everywhere?

如果你的应用类似于此站点,并且大多数交互都很粗糙(例如替换页面或整个部分),则通常不需要记忆化。另一方面,如果你的应用更像是绘图编辑器,并且大多数交互都是颗粒状的(如移动形状),那么你可能会发现记忆化非常有用。

¥If your app is like this site, and most interactions are coarse (like replacing a page or an entire section), memoization is usually unnecessary. On the other hand, if your app is more like a drawing editor, and most interactions are granular (like moving shapes), then you might find memoization very helpful.

仅当你的组件经常使用完全相同的属性重新渲染并且其重新渲染逻辑非常昂贵时,使用 memo 进行优化才有价值。如果你的组件重新渲染时没有明显的延迟,则 memo 是不必要的。请记住,如果传递给组件的属性 总是不同的,例如传递一个对象或在渲染期间定义的普通函数,那么 memo 是完全无用的。这就是为什么你经常需要 useMemouseCallback 以及 memo

¥Optimizing with memo is only valuable when your component re-renders often with the same exact props, and its re-rendering logic is expensive. If there is no perceptible lag when your component re-renders, memo is unnecessary. Keep in mind that memo is completely useless if the props passed to your component are always different, such as if you pass an object or a plain function defined during rendering. This is why you will often need useMemo and useCallback together with memo.

在其他情况下,将组件封装在 memo 中没有任何好处。这样做也没有什么大不了的,所以一些团队选择不考虑个别情况,并尽可能多地记忆化。这种方法的缺点是代码变得不那么可读了。此外,并非所有的记忆化都是有效的:单个值 “总是新的” 足以破坏整个组件的记忆化。

¥There is no benefit to wrapping a component in memo in other cases. There is no significant harm to doing that either, so some teams choose to not think about individual cases, and memoize as much as possible. The downside of this approach is that code becomes less readable. Also, not all memoization is effective: a single value that’s “always new” is enough to break memoization for an entire component.

在实践中,你可以通过遵循一些原则来避免大量记忆化:

¥In practice, you can make a lot of memoization unnecessary by following a few principles:

  1. 当一个组件在视觉上封装其他组件时,让它 接受 JSX 作为子级。 这样,当封装器组件更新自己的状态时,React 知道它的子级不需要重新渲染。

    ¥When a component visually wraps other components, let it accept JSX as children. This way, when the wrapper component updates its own state, React knows that its children don’t need to re-render.

  2. 首选本地状态,除非必要,否则不要 提升状态。例如,不要保留像表单这样的瞬时状态,也不要保留某个条目是否悬停在树的顶部或全局状态库中。

    ¥Prefer local state and don’t lift state up any further than necessary. For example, don’t keep transient state like forms and whether an item is hovered at the top of your tree or in a global state library.

  3. 保留你的 渲染逻辑纯粹。 如果重新渲染组件导致问题或产生一些明显的视觉伪像,那是你的组件中的错误!修复错误而不是添加记忆化。

    ¥Keep your rendering logic pure. If re-rendering a component causes a problem or produces some noticeable visual artifact, it’s a bug in your component! Fix the bug instead of adding memoization.

  4. 避免 更新状态的不必要的副作用。 React 应用中的大多数性能问题都是由副作用引起的更新链引起的,这些更新链导致组件反复渲染。

    ¥Avoid unnecessary Effects that update state. Most performance problems in React apps are caused by chains of updates originating from Effects that cause your components to render over and over.

  5. 尝试以 从你的副作用中删除不必要的依赖。 例如,在副作用内部或组件外部移动某些对象或函数通常更简单,而不是记忆化。

    ¥Try to remove unnecessary dependencies from your Effects. For example, instead of memoization, it’s often simpler to move some object or a function inside an Effect or outside the component.

如果特定的交互仍然感觉滞后,使用 React 开发者工具分析器 看看哪些组件可以从记忆化中获益最多,并在需要的地方添加记忆化。这些原则使你的组件更易于调试和理解,因此在任何情况下都遵循它们是很好的。从长远来看,我们正在研究 自动进行粒度记忆化 以一劳永逸地解决这个问题。

¥If a specific interaction still feels laggy, use the React Developer Tools profiler to see which components would benefit the most from memoization, and add memoization where needed. These principles make your components easier to debug and understand, so it’s good to follow them in any case. In the long term, we’re researching doing granular memoization automatically to solve this once and for all.


使用状态更新记忆化组件

¥Updating a memoized component using state

即使一个组件被记忆化了,它仍然会在它自己的状态改变时重新渲染。记忆化只与从父级传递给组件的属性有关。

¥Even when a component is memoized, it will still re-render when its own state changes. Memoization only has to do with props that are passed to the component from its parent.

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log('Greeting was rendered at', new Date().toLocaleTimeString());
  const [greeting, setGreeting] = useState('Hello');
  return (
    <>
      <h3>{greeting}{name && ', '}{name}!</h3>
      <GreetingSelector value={greeting} onChange={setGreeting} />
    </>
  );
});

function GreetingSelector({ value, onChange }) {
  return (
    <>
      <label>
        <input
          type="radio"
          checked={value === 'Hello'}
          onChange={e => onChange('Hello')}
        />
        Regular greeting
      </label>
      <label>
        <input
          type="radio"
          checked={value === 'Hello and welcome'}
          onChange={e => onChange('Hello and welcome')}
        />
        Enthusiastic greeting
      </label>
    </>
  );
}

如果你将状态变量设置为其当前值,即使没有 memo,React 也会跳过重新渲染你的组件。你可能仍然会看到你的组件函数被额外调用一次,但结果将被丢弃。

¥If you set a state variable to its current value, React will skip re-rendering your component even without memo. You may still see your component function being called an extra time, but the result will be discarded.


使用上下文更新记忆化组件

¥Updating a memoized component using a context

即使组件被记忆化,它仍然会在它使用的上下文发生变化时重新渲染。记忆化只与从父级传递给组件的属性有关。

¥Even when a component is memoized, it will still re-render when a context that it’s using changes. Memoization only has to do with props that are passed to the component from its parent.

import { createContext, memo, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('dark');

  function handleClick() {
    setTheme(theme === 'dark' ? 'light' : 'dark'); 
  }

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={handleClick}>
        Switch theme
      </button>
      <Greeting name="Taylor" />
    </ThemeContext.Provider>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  const theme = useContext(ThemeContext);
  return (
    <h3 className={theme}>Hello, {name}!</h3>
  );
});

要使你的组件仅在某些上下文的一部分发生变化时才重新渲染,请将你的组件一分为二。从外部组件的上下文中读取你需要的内容,并将其作为属性传递给已记忆化的子级。

¥To make your component re-render only when a part of some context changes, split your component in two. Read what you need from the context in the outer component, and pass it down to a memoized child as a prop.


最小化属性变化

¥Minimizing props changes

当你使用 memo 时,你的组件会在任何属性与之前不相等时重新渲染。这意味着 React 使用 Object.is 比较将组件中的每个属性与其之前的值进行比较。请注意,Object.is(3, 3)true,但 Object.is({}, {})false

¥When you use memo, your component re-renders whenever any prop is not shallowly equal to what it was previously. This means that React compares every prop in your component with its previous value using the Object.is comparison. Note that Object.is(3, 3) is true, but Object.is({}, {}) is false.

要充分利用 memo,请尽量减少属性更换的次数。例如,如果属性是一个对象,则通过使用 useMemo 来防止父组件每次都重新创建该对象

¥To get the most out of memo, minimize the times that the props change. For example, if the prop is an object, prevent the parent component from re-creating that object every time by using useMemo:

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);

const person = useMemo(
() => ({ name, age }),
[name, age]
);

return <Profile person={person} />;
}

const Profile = memo(function Profile({ person }) {
// ...
});

最小化属性更改的更好方法是确保组件在其属性中接受最少的必要信息。例如,它可以接受单个值而不是整个对象:

¥A better way to minimize props changes is to make sure the component accepts the minimum necessary information in its props. For example, it could accept individual values instead of a whole object:

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}

const Profile = memo(function Profile({ name, age }) {
// ...
});

甚至单个值有时也可以投射到那些变化不那么频繁的值上。例如,这里的组件接受一个布尔值,指示值的存在而不是值本身:

¥Even individual values can sometimes be projected to ones that change less frequently. For example, here a component accepts a boolean indicating the presence of a value rather than the value itself:

function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}

const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});

当你需要将函数传递给已记忆化的组件时,要么在组件外部声明它以使其永不更改,要么 useCallback 在重新渲染之间缓存其定义。

¥When you need to pass a function to memoized component, either declare it outside your component so that it never changes, or useCallback to cache its definition between re-renders.


指定自定义比较函数

¥Specifying a custom comparison function

在极少数情况下,可能无法最小化记忆组件的属性更改。在这种情况下,你可以提供一个自定义比较函数,React 将使用它来比较新旧属性,而不是使用浅层相等。该函数作为第二个参数传递给 memo。只有当新属性与旧属性产生相同的输出时,它才应该返回 true;否则它应该返回 false

¥In rare cases it may be infeasible to minimize the props changes of a memoized component. In that case, you can provide a custom comparison function, which React will use to compare the old and new props instead of using shallow equality. This function is passed as a second argument to memo. It should return true only if the new props would result in the same output as the old props; otherwise it should return false.

const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}

如果这样做,请使用浏览器开发者工具中的性能面板来确保你的比较功能实际上比重新渲染组件更快。你可能会感到惊讶。

¥If you do this, use the Performance panel in your browser developer tools to make sure that your comparison function is actually faster than re-rendering the component. You might be surprised.

当你进行性能测量时,请确保 React 在生产模式下运行。

¥When you do performance measurements, make sure that React is running in the production mode.

易犯错误

如果你提供自定义 arePropsEqual 实现,则必须比较每个 prop,包括函数。函数通常会 掩盖 父组件的属性和 state。如果你在 oldProps.onClick !== newProps.onClick 时返回 true,你的组件将在 onClick 处理程序中保留 “seeing” 之前渲染的属性和状态,从而导致非常混乱的错误。

¥If you provide a custom arePropsEqual implementation, you must compare every prop, including functions. Functions often close over the props and state of parent components. If you return true when oldProps.onClick !== newProps.onClick, your component will keep “seeing” the props and state from a previous render inside its onClick handler, leading to very confusing bugs.

避免在 arePropsEqual 内部进行深度相等性检查,除非你 100% 确定你正在使用的数据结构具有已知的有限深度。深度相等检查可能会变得非常慢,并且如果有人稍后更改数据结构,可能会冻结你的应用很多秒。

¥Avoid doing deep equality checks inside arePropsEqual unless you are 100% sure that the data structure you’re working with has a known limited depth. Deep equality checks can become incredibly slow and can freeze your app for many seconds if someone changes the data structure later.


故障排除

¥Troubleshooting

当属性是对象、数组或函数时,我的组件会重新渲染

¥My component re-renders when a prop is an object, array, or function

React 通过浅相等来比较新旧属性:也就是说,它考虑每个新属性是否引用等于旧属性。如果每次重新渲染父级时都创建一个新对象或数组,即使各个元素都相同,React 仍会认为它已更改。同样,如果你在渲染父组件时创建了一个新的函数,即使函数定义相同,React 也会认为它发生了变化。为了避免这种情况,在父组件中简化属性或 memoize 属性

¥React compares old and new props by shallow equality: that is, it considers whether each new prop is reference-equal to the old prop. If you create a new object or array each time the parent is re-rendered, even if the individual elements are each the same, React will still consider it to be changed. Similarly, if you create a new function when rendering the parent component, React will consider it to have changed even if the function has the same definition. To avoid this, simplify props or memoize props in the parent component.


React 中文网 - 粤ICP备13048890号