核心概念
首先,要贯彻一个思想:
唯有 State 改变才会引入 React 重渲染
也就是说,其实 props
和 context
的改变并不会导致重渲染。其实是他们的父级组件 state
改变了,导致所有后代组件都进行了一次重渲染。考虑一个这样的组件结构:
import React from "react";
function App() {
return (
<>
<Counter />
<footer>
<p>Copyright 2022 Big Count Inc.</p>
</footer>
</>
);
}
function Counter() {
const [count, setCount] = React.useState(0);
return (
<main>
<BigCountNumber count={count} />
<button onClick={() => setCount(count + 1)}>Increment</button>
</main>
);
}
function BigCountNumber({ count }) {
return (
<p>
<span className="prefix">Count:</span>
{count}
</p>
);
}
export default App;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
当 count
变量改变时,Couter
的后代 BingCountNumber
也会被重新渲染。但父级组件 App
是不会被重新渲染的,也包括 App
的后代之一 footer
。
React 渲染机制
我们可以把每次渲染都当作 React 在拍一张照片(snapshot),这张照片描述了最后 html 生成的结构。当 count
变量改变时,React 会重新拍摄一张照片并进行比对,然后只更新有变化的地方。
但如上节所说,只要某个组件的 state 发生变化,那么不管其后代组件有没有 props,React 会把它们都更新个遍。
为什么 React 要这么设计呢?因为,React 并不知道你的组件是不是「每次渲染都能输出一样的结果」,就比如:
function CurrentTime() {
const now = new Date();
return <p>It is currently {now.toString()}</p>;
}
- 1
- 2
- 3
- 4
你会发现,这个组件无论 props 是什么,每次渲染的结果都是不一样的。这种组件被称为「副作用组件」,与之相反的概念是「纯组件」,也就是在相同输入下,输出也相同的组件。
React 开发团队在渲染结果方面一直都是秉承小心谨慎的态度,宁愿牺牲一些性能也不愿意冒风险渲染出不正确的页面(比如 React 在捕获到错误的时候会默认白屏)。
创建纯组件
如果我们可以自信地认为自己写的组件就是纯组件,每次渲染输出的结果都是相同的。那么可以使用 方法包装起来:
function Decoration() {
return <div className="decoration">⛵️</div>;
}
export default React.memo(Decoration);
- 1
- 2
- 3
- 4
这样在它的父级组件的重渲染的时候,只要 props 没变化,那么这个组件就不会被重新渲染。
那为什么 React 不把这个行为内置到框架中呢?这个逻辑感觉不是更直观,更理所当然吗?原因是性能。
作为开发者我们总是高估了「重渲染」的成本,其实 React 经过这么长时间的升级,已经把重渲染这一步优化得很好了。如果一个组件,没有很耗时的计算,但却有很多 props 依赖,那么其实新旧 props 的对比的性能损耗反而会比重新渲染更高。
反之,如果一个组件拥有比较复杂的计算逻辑,但 props 数量不多,那还是建议使用 来减少无谓的计算。
不过,这个现象可能会在未来被改变,React 官方已经在测试「自动 memo」相关的功能了。详情可以看黄玄的演讲。
Context 的影响
说完 props,再来说说 context:
const GreetUser = React.memo(() => {
const user = React.useContext(UserContext);
if (!user) {
return "Hi there!";
}
return `Hello ${user.name}!`;
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
其实 context 可以被看作隐性的 props。也就是说,上面的代码可以看成下面这样:
const GreetUser = React.memo(({ user }) => {
if (!user) {
return "Hi there!";
}
return `Hello ${user.name}!`;
});
- 1
- 2
- 3
- 4
- 5
- 6
那么这个组件的重渲染规则就和上面说的 props 如出一辙。
调试
使用 React Developer Tools
可以调试重渲染现象。在开发者工具中选择 Profiler
面板,可以录制一段活动来分析组件的详细渲染情况。
或者可以实时高亮被重渲染的组件。
例外
有时候可能会碰到一种情况:明明包裹了 并且 props 也「没有改变」,子组件还是被重渲染了:
function App() {
const dog = {
name: "Spot",
breed: "Jack Russell Terrier",
};
return <DogProfile dog={dog} />;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这是因为 对于 props 的比较实际上是浅比较,也就是说如果 props 是一个 Object,那只会比较其内存的引用地址。
在 ???? 上面的那个例子中,每次 App 渲染都会创建一个新的 dog
对象并传入 DogProfile
,那么不管 DogProfile
有没有包裹 ,都会进行重渲染。
总结
- 只有 state 变化会导致 React 组件重渲染。
- 使用
创建纯组件
- 不要过度优化
-
只对 props 进行浅比较
本文源自:/react/why-react-re-renders/