前面说过,React 每次渲染组件,都会执行组件函数。现在,我们来看看,React 会在什么时候渲染组件,也就是什么时候会执行你的组件函数。
组件函数执行的 3 种情况
你的组件函数会在以下 3 种情况下执行:
- 组件首次挂载时
- 组件的状态或依赖的 Context 发生变化时
- 父组件重新渲染时
第 3 种情况,有必要单独说一下。当 React 更新一个组件的时候,会连带者更新它的子组件,即使子组件的 props 没有发生变化。也就是说,React 更新的是整个组件树,而不是单个组件。
有个普遍的错误说法是 "props 变化时,组件会更新"。第一,props 变化,意味着父组件更新了,此时组件更新是因为父组件更新导致的,不管 props 有没有变化,组件都会更新。其次,如果按这个理解,那你说下面这种情况组件会更新吗?
jsx
import { createRoot } from "react-dom/client";
let sec = 0;
setInterval(() => {
sec++;
}, 1000);
function Timer({ secPassed }) {
return <p>Seconds passed: {secPassed}</p>;
}
createRoot(document.getElementById("root")).render(<Timer secPassed={sec} />);
性能问题
你可能发现了,父组件更新,所有的后代组件也会一起更新,那性能是不是会有问题?尤其是对那些 props 没有变化的后端组件,执行它们不是浪费吗?
从性能上看这确实是一个浪费。不过,实践证明,在大部分项目下,这种性能损耗不会导致明显的性能问题,不然 React 已经被喷惨了。所以,我们平时不用担心性能问题。
不会引发性能问题,很大一个原因是因为大部分的组件函数都很简单,很快就执行完了。一般的组件函数都是做一些变量的声明、读取、赋值,然后创建并返回 React 元素(JSX)。
这也告诉我们,尽量让组件函数简单,把一些具有较高复杂度的计算放在 useMemo
中,避免不必要的计算。
另一个需要关注的是组件的数量,比如:
- 大量使用某个组件。比如,展示大量数据,每个数据对应一个
<ListItem />
。 - 一个组件有大量的后代组件,而且它自身频繁更新。
这类情况,在每次更新时,都会执行大量的组件函数,容易导致性能问题。可以使用 React.memo
,当组件的 props 没有变化时,跳过组件的更新。
React.memo
memo
可以让我们在更新前,比较新旧两次的 props,决定要不要执行更新。
js
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
默认情况下,是对每个 prop 做浅比较,如果存在对象结构的 prop,一般使用 lodash.isEqual
来做深比较,如果含有函数,还需要注意保证先后两次函数的引用不发生变化,不然即使 lodash.isEqual
也会返回 false
。
memo
优化的是第 3 种情况,也就是父组件更新都会导致子组件更新的情况。对于第 1、2 种情况,组件更新是不能也不可避免的。
memo
可以提升性能,但是,不要滥用,除非你有理由认为这个组件的频繁更新会导致性能问题。
最后,应该在组件使用的时候,才决定要不要使用 memo
,不要在组件定义的时候就使用 memo
。假如你对外提供组件,你不需要在导出之前使用 memo
。因为你写组件的时候,不知道父组件会不会频繁更新,也不知道组件会不会被大量使用,也不知道如何有效比较两次 props,这些只有使用的时候才知道。
总结
- 函数组件会在 3 中情况下执行:
- 组件首次挂载时
- 组件的状态或依赖的 Context 发生变化时
- 父组件重新渲染时
- 父组件更新时,子组件默认都会更新,即使子组件的 props 没有变化。
- 这种冗余的更新,在大部分情况下,不会导致明显的性能问题。
- 为了避免性能问题,记住:
- 尽量保持组件简单。
- 在组件内如果有高复杂度的计算,使用
useMemo
。 - 对于大量使用的组件,或者更新繁重的组件,可以在必要时使用
React.memo
。
- 组件自身的状态或依赖的 Context 变化时,组件一定会更新,
React.memo
不会影响这种情况。