为什么React会Re-Render?【译】
来源www.joshwcomeau.com/react/why-r…
正文
1、背景
很多人不知道:
- React-re-rendering重新渲染是怎么工作
- 是什么触发了React的重渲染
如果我们不理解React渲染周期的原理,那么我们如何去理解React.memo或者useCallback这些Hook呢
下面我们对【React什么时候、为什么进行重新渲染】进行讲解,
2、React Loop的核心
首先一个基本事实:在React中的每一个重新渲染都伴随着一个state状态的改变,这是唯一一个可以触发React组件重新渲染的trigger
但是上面会不会听起来不太正确?毕竟组件的props参数改变难道不会引起组件的重新渲染吗?context上下文的改变难道不会引起重新渲染吗?
还有一句话:当一个组件重新渲染的时候,它的子代也同样会进行重渲染
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;
App组件中有一个子代Counter,Counter里面又有一个子弟BigCountNumber
在React中,每一个state变量都依附在了一个特定的组件实例上,上面的代码中,我们仅有count这个state,这个state和我们的Counter组件相关联。
当该count 变量改变的时候,Counter组件会重新渲染,BigCountNumber
这个组件也会因为Counter重新渲染而重新渲染
当count改变的时候,Counter和BigCountNumber组件会重新渲染,但是外层的App不会重新渲染
所以现在我们澄清一个误解一:当一个state变量改变的时候整个应用都会重渲染
一些开发者会认为在React中每一个state的改变都会强制让整个应用都进行渲染,这个其实是不对的,重新渲染仅仅影响和当前state相关联的组件,以及该组件的后代组件。
那么为什么它要酱紫实现呢?
React的主要工作是保证应用的UI展示是和React state同步的,re-render重新渲染的意义其实是计算出UI的哪些内容被改变了。
让我们用上面的Counter案例来解释,当应用第一次挂载的时候,React渲染所有的组件并且计算出了下面这个HTML,表示DOM应该展现出什么样子的草图
<main>
<p>
<span class="prefix">Count:</span>
0
</p>
<button>
Increment
</button>
</main>
<footer>
<p>Copyright 2022 Big Count Inc.</p>
</footer>
当用户点击按钮的时候,count变量就从0变成1了,这个就会导致我们UI上面显示的Count值改变了。
React就会为Counter和BigCountNumber
这两个组件重新运行代码,然后我们就可以生成一个新的sketch草图。
<main>
<p>
<span class="prefix">Count:</span>
1
</p>
<button>
Increment
</button>
</main>
<footer>
<p>Copyright 2022 Big Count Inc.</p>
</footer>
每一次的render渲染都是一个snapshot快照,就好像一个照片一样,这个照片就给我们展示了基于当前应用的state UI应该长什么样。
React就好像玩了一个“找不同”游戏一样去计算出新旧两个快照之间有上面不同,所以上面的例子中,两个render产生的快照里面就有一个不同,就是展示Count数值的text node从0变成了1,所以就通过修改这个text node去匹配上快照就行,完成了这个工作之后,React就“回到位置上”等待下一次state的改变。
以上就是React Loop的核心
因为count这个state是和我们Counter组件相关联的,所以直接影响了Counter,并且还要对Counter子代组件进行重新渲染,因为BigCountNumber组件负责展示了count的值,如果我们不重新渲染BigCountNumber这个组件的话,我们就不会知道我们快照里面的text node应该从0变成1,所以我们需要把BigCountNumber组件加入到我们的快照里面
所以re-render重新渲染的目的其实是计算出state的改变影响了哪些用户UI视图,为此我们需要去重新渲染所有 潜在影响的组件,这样才能得到一个准确的snapshot快照
3、重渲染难道不是因为组件的props改变吗?
第二个大误解:一个组件的参数props改变了,那么这个组件将进行re-render重新渲染
例子:
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>
<Decoration />
</main>
);
}
function BigCountNumber({ count }) {
return (
<p>
<span className="prefix">Count:</span>
{count}
</p>
);
}
function Decoration() {
return (
<div className="decoration">
GOGOCJ
</div>
);
}
export default App;
例子中我们添加了一个Decoration组件,里面是一个写死的文本GOGOCJ,并没有依赖于count变量,那么当我们改变count变量的时候,它会不会重新渲染呢?
实验发现,会重新渲染
当一个组件重新渲染的时候,它会尝试去对它所有的子代都进行重新渲染,不管是否有state作为props或者没有,都会进行重新渲染。
那么我们并没有传递count这个参数去组件,为什么它需要重新渲染?
答:因为React很难百分百确定Decoration这个组件会不会直接或者间接的去依赖于count这个state变量。
①、在理想情况下,React组件应该总是pure的(纯函数、纯组件)
pure组件就意味着给相同的参数得到的是一样的UI结果、函数结果
②、但是真实情况是,我们很多的组件都是不纯的(impure),比如下面这个就是一个不使用state的非纯组件:
function CurrentTime() {
const now = new Date();
return (
<p>It is currently {now.toString()}</p>
);
}
每次渲染的时候当前组件都将展示不同的value值,因为当前组件依赖于当前时间。
假如我们使用ref去操作了一个本来是pure的子元素,那么React将永远没有办法去分清楚到底是pure还是impure,所以最保守的方式就是:React不做这个pure的判断,默认是重新渲染所有的子代,而吧这个设置一个组件是不是纯组件的权力交给了程序员。
4、创建一个纯组件
React.memo 和 React.PureComponent 类组件,这两种方式都可以让我们忽略重新渲染
function Decoration() {
return (
<div className="decoration">
⛵️
</div>
);
}
export default React.memo(Decoration);
通过React.memo包裹的组件就“告诉”React:当前组件是纯组件,放心吧,你不需要重新渲染,除非这个组件的props参数改变了“
这是一种memoization记忆化技术
这种方式就会让React将记住之前的snapshot,如果当前组件没有props改变的话,React将复用之前的snapshot快照而不是去重新的生成一个快照。
你可能会酱紫想:为什么这种记忆化模式要程序员做,而不是React在编译的时候进行识别作为一个默认的优化方式?
但是其实React的开发团队已经在积极的调研是否可以在对React代码编译时期进行【”auto-memoize“自动判断是否是纯组件并进行备份】的可能性,目前还是试验阶段,但是初期实验效果显著
5、那如果是通过Context上下文传递state呢?
默认情况下,当前state改变了,就会改变所在组件的所有子组件,即使是通过context把state传递到后代组件的也是一样会重新进行渲染的
例如:
const GreetUser = React.memo(() => {
const user = React.useContext(UserContext);
if (!user) {
return "Hi there!";
}
return `Hello ${user.name}!`;
});
这里有一个纯组件使用了UserContext这个上下文,在这个例子中GreetUser组件是一个纯组件,因为没有使用props,但是它有一个”看不到“的依赖:通过context把一个存在于React state里面的user变量传递过来了
如果user这个state改变了,将发生一次重新渲染的操作,GreetUser也将要产生一个新的snapshot快照,因为React知道这个组件使用了一个额外的context,React会认为这个context其实也是一个props,props改变了那么就要重新进行渲染了
英语生词
-
get by 得到、过得去,应付过去
- We understand enough to get by 我们懂得足够多,可以应付过去
-
a handful of 一把、少量的、少部分的
-
hand-wavy 简短的
- hand-wavy answers 简短的回答
-
misconceptions 误解
-
uncertainty 不确定性
-
tutorial 教程
-
mental 思考的、精神的
-
Intended audience 阅读对象
- intended 语气的、故意的、为...打算的
-
intermediate 中间的,中等水平的
-
bookmark 书签
-
fundamental 基本的
- fundamental truth 基本的真理
-
that probably doesn't sound right... after all
- 听起来不太对,毕竟。。。
-
descendants 后代
-
interactive graph 交互图
-
mechanic 机械工、手段 、方法
-
comes up with 提出
-
sketch 草图
-
flip 翻转
- flip from ... to ...
-
brand new 全新的
-
liberty 自由
-
aside from 除了
-
no quite 不完全是
- quite 相当
-
regardless 不管
-
intuitive 直观的、直觉的、易懂的
-
sneakier 隐蔽的,鬼鬼祟祟的,卑鄙的
-
mutated 突变
-
stale 不新鲜的
-
cascade 倾泻、连续传递
-
in order 为了,整理,有秩序的
-
tweak 调整、微调
-
sorta 近似,有几分
-
going through the trouble of 经历什么的麻烦
-
pretend 假装
-
exact 确切的,精确的
-
snap 断裂 折断,咔嚓
- snap a picture 拍了一张照片
-
live-code 实况代码
-
wondering 想知道
-
improve performance 提高性能
-
overestimate 高估
-
a bunch of 一群
-
counter-productive 适得其反
- counter计算器
- productive 富有成效的
-
circumstances 场景、情况
-
investigate 调查
-
complicate 复杂的
-
stuff 东西、物品
-
more-or-less 或多或少
-
consumes 消耗 使用
-
Profiling 分析 资料收集
- Profile 人的外形 轮廓,侧面
-
trick 技巧 花招
-
profiler 分析器
-
subtle 微妙的,不易察觉的
-
mind-bending 离奇古怪的
- bend 弯曲
-
wrack 破坏、毁坏、沉船
-
havoc 灾难、造成严重破坏
-
inscrutable 神秘的,不可理解的
-
confession 供认、承认、坦白
-
pluck 摘录
-
modality 形式、形态
-
milliseconds 毫秒
-
percentile 百分位
-
tempting 吸引人的、诱人的
-
spree 疯狂、放纵
-
sluggish 缓慢的、迟钝的
-
aside 一边、旁边、除什么之外