memo
允许你在组件的 props 未更改时跳过重新渲染。
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
参考
memo(Component, arePropsEqual?)
将组件包装在 memo
中以获取该组件的*已记忆*版本。只要其 props 没有更改,此已记忆版本的组件通常不会在其父组件重新渲染时重新渲染。但 React 仍然可能会重新渲染它:记忆是一种性能优化,而不是保证。
import { memo } from 'react';
const SomeComponent = memo(function SomeComponent(props) {
// ...
});
参数
-
Component
:要记忆的组件。memo
不会修改此组件,而是返回一个新的、已记忆的组件。接受任何有效的 React 组件,包括函数和forwardRef
组件。 -
可选
arePropsEqual
:一个接受两个参数的函数:组件的先前 props 和其新 props。如果旧 props 和新 props 相等,它应该返回true
:也就是说,如果组件使用新 props 渲染的输出和行为与使用旧 props 相同。否则它应该返回false
。通常,您不会指定此函数。默认情况下,React 将使用Object.is
比较每个 prop。
返回值
memo
返回一个新的 React 组件。它的行为与提供给 memo
的组件相同,只是除非其 props 已更改,否则 React 不会始终在其父组件重新渲染时重新渲染它。
用法
当 props 不变时跳过重新渲染
React 通常会在父组件每次重新渲染时重新渲染子组件。使用 memo
,你可以创建一个组件,只要它的新 props 与旧 props 相同,React 就不会在父组件重新渲染时重新渲染它。这样的组件被称为已_记忆_的。
要记忆一个组件,请将其包裹在 memo
中,并使用它返回的值来代替原始组件
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});
export default Greeting;
React 组件应该始终具有 纯渲染逻辑。 这意味着如果它的 props、state 和 context 没有改变,它必须返回相同的输出。通过使用 memo
,你是在告诉 React 你的组件符合此要求,因此只要它的 props 没有改变,React 就不需要重新渲染。即使使用了 memo
,如果组件自身的 state 发生变化,或者它正在使用的 context 发生变化,你的组件仍然会重新渲染。
在本例中,请注意,每当 name
发生变化时(因为它是其 props 之一),Greeting
组件都会重新渲染,但当 address
发生变化时,它不会重新渲染(因为它没有作为 prop 传递给 Greeting
)
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>; });
深入探讨
如果你的应用像本网站一样,并且大多数交互都是粗粒度的(例如替换页面或整个部分),通常不需要记忆。另一方面,如果你的应用更像是一个绘图编辑器,并且大多数交互都是细粒度的(例如移动形状),那么你可能会发现记忆非常有用。
仅当你的组件经常使用相同的 props 重新渲染,并且其重新渲染逻辑很耗时时,使用 memo
进行优化才有价值。如果你的组件重新渲染时没有明显的延迟,则不需要 memo
。请记住,如果传递给组件的 props _始终不同_,例如,如果你传递在渲染期间定义的对象或普通函数,则 memo
将完全无用。这就是为什么你经常需要将 useMemo
和 useCallback
与 memo
一起使用。
在其他情况下,将组件包裹在 memo
中没有任何好处。这样做也没有什么明显的坏处,因此有些团队选择不去考虑个别情况,而是尽可能多地进行记忆。这种方法的缺点是代码变得难以阅读。而且,并非所有记忆都是有效的:一个“始终是新”的值就足以破坏整个组件的记忆。
在实践中,你可以通过遵循一些原则来避免许多不必要的记忆
- 当一个组件在视觉上包裹其他组件时,让它 接受 JSX 作为子组件。 这样,当包装器组件更新其自身状态时,React 知道它的子组件不需要重新渲染。
- 首选本地 state,不要将 state 提升 超过必要的高度。例如,不要将表单和项目是否悬停等瞬态 state 保留在树的顶部或全局状态库中。
- 保持你的 渲染逻辑纯净。 如果重新渲染组件会导致问题或产生一些明显的视觉伪像,那就是组件中的错误!修复错误,而不是添加记忆。
- 避免 更新 state 的不必要的 Effect。 React 应用中的大多数性能问题都是由源自 Effect 的更新链导致的,这些更新会导致你的组件反复渲染。
- 尝试 从 Effect 中删除不必要的依赖项。 例如,与记忆相比,将某些对象或函数移动到 Effect 内部或组件外部通常更简单。
如果特定的交互仍然感觉滞后,请 使用 React 开发工具分析器 查看哪些组件将从记忆中受益最多,并在需要的地方添加记忆。这些原则使你的组件更易于调试和理解,因此无论如何都应该遵循它们。从长远来看,我们正在研究 自动进行细粒度记忆 以一劳永逸地解决这个问题。
使用 state 更新已记忆的组件
即使组件已被记忆,当其自身 state 发生变化时,它仍然会重新渲染。记忆只与从父组件传递给组件的 props 有关。
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> </> ); }
如果你将 state 变量设置为其当前值,即使没有 memo
,React 也会跳过重新渲染你的组件。你可能仍然会看到你的组件函数被额外调用了一次,但结果将被丢弃。
使用 context 更新已记忆的组件
即使组件被记忆化了,当它使用的上下文发生变化时,它仍然会重新渲染。记忆化只与从父组件传递给组件的 props 有关。
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> ); });
要使组件仅在某些上下文的一部分发生变化时才重新渲染,请将组件拆分为两个。在外层组件中读取您需要的上下文,并将其作为 prop 传递给记忆化的子组件。
最小化 props 更改
当您使用 memo
时,每当任何 prop 与之前的 prop 浅层不等 时,您的组件都会重新渲染。这意味着 React 会使用 Object.is
比较将组件中的每个 prop 与其先前的值进行比较。请注意,Object.is(3, 3)
为 true
,但 Object.is({}, {})
为 false
。
为了充分利用 memo
,请尽量减少 props 更改的次数。例如,如果 prop 是一个对象,请使用 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 }) {
// ...
});
最小化 props 更改的更好方法是确保组件在其 props 中接受最少的必要信息。例如,它可以接受单个值而不是整个对象
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 }) {
// ...
});
即使是单个值,有时也可以将其投影为变化频率较低的值。例如,这里有一个组件接受一个布尔值来指示值的存在,而不是值本身
function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}
const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});
当您需要将函数传递给记忆化组件时,请在组件外部声明它,使其永不更改,或者使用 useCallback
在重新渲染之间缓存其定义。
指定自定义比较函数
在极少数情况下,可能无法最小化记忆化组件的 props 更改。在这种情况下,您可以提供一个自定义比较函数,React 将使用它来比较旧 prop 和新 prop,而不是使用浅层相等。此函数作为第二个参数传递给 memo
。只有当新 prop 会导致与旧 prop 相同的输出时,它才应返回 true
;否则,它应该返回 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;
})
);
}
如果您这样做,请使用浏览器开发者工具中的“性能”面板确保您的比较函数实际上比重新渲染组件更快。您可能会感到惊讶。
进行性能测量时,请确保 React 以生产模式运行。
故障排除
当 prop 是对象、数组或函数时,我的组件会重新渲染
React 通过浅层相等性比较旧 prop 和新 prop:也就是说,它会考虑每个新 prop 是否与旧 prop 引用相等。如果您每次重新渲染父组件时都创建一个新的对象或数组,即使各个元素都相同,React 仍然会认为它已更改。类似地,如果您在渲染父组件时创建一个新函数,即使该函数具有相同的定义,React 也会认为它已更改。为避免这种情况,请简化 props 或在父组件中记忆化 props。