生活就是当你忙着制定其他计划时,发生在你身上的事情 - 约翰·列侬
大家好,我是柒八九。
前言
Dan
的文章在使用React.memo之前的注意事项中,通过几个例子来描述,有时候我们可以通过组件组合的方式来优化组件的多余渲染。文章中提到要么通过将下放State,要么将内容提升。因为组件组合是React
的自然思维模式。正如Dan所指出的,这也将与Server Components非常契合。
然后,在各种文章中,都提倡克制useMemo
的使用,优先使用组件组合来处理组件冗余渲染的问题。但是,它们都没讲明白,遇到这些问题,为什么不首选使用React.memo
呢?
最核心的点,就是
Memo
很容易被破坏
下面,我们就由浅入深的来窥探其中的门道。
好了,天不早了,干点正事哇。
你能所学到的知识点
- 前置知识点
- 问题复现
- children
- 替代方案
- 问题的根源
前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
Object.is
Object.is
是 JavaScript
中的一个内建函数,用于比较两个值是否严格相等。它的作用类似于严格相等操作符 ===
,但有一些关键区别。
语法
Object.is(value1, value2)
参数
value1
:比较的第一个值。value2
:比较的第二个值。
返回值
Object.is
返回一个布尔值
,表示两个值是否严格相等。
特点
- NaN 相等性:
Object.is
在比较NaN
值时与其他方法不同。它认为Object.is(NaN, NaN)
为true
,而严格相等操作符===
认为NaN === NaN
为false
。
Object.is(NaN, NaN); // true NaN === NaN; // false
- +0 和 -0 不相等:
Object.is
能够区分正零
和负零
,即Object.is(+0, -0)
返回false
,而===
会认为它们相等。
Object.is(+0, -0); // false +0 === -0; // true
- +0 和 -0 与0相等: 除了自身之外,正零和负零都与其他数字相等。
Object.is(+0, 0); // true Object.is(-0, 0); // true
- 其它值的比较: 对于其他值,
Object.is
表现与===
相同。
Object.is(1, 1); // true Object.is('foo', 'foo'); // true
用途
Object.is
主要用于比较两个值,特别是在需要明确处理 NaN
或区分正零和负零时。这可以用于创建更精确的相等性检查,而不受 JavaScript
中一些奇怪的行为的影响。例如,当比较浮点数或需要区分 NaN
时,Object.is
可能更有用。
function areTheyEqual(value1, value2) { return Object.is(value1, value2); } areTheyEqual(NaN, NaN); // true areTheyEqual(+0, -0); // false
Record 和Tuple
它们属于ECMAScript
提案Records and Tuples。
Record
(记录):这将是一种深度不可变的类对象结构
,与普通JavaScript
对象不同,其属性和值将是不可变的。这将有助于避免对象的属性被无意中更改。Tuple
(元组):这将是一种深度不可变的类数组结构
,与普通JavaScript
数组不同,其元素将是不可变的。这将有助于确保元组的内容在创建后不可更改。
这些看起来类似于普通的对象和数组,但它们具有以“#”前缀为特征:
const record = #{a: 1, b: 2}; record.a; // 1 const updatedRecord = #{...record, b: 3}; // #{a: 1, b: 3}; const tuple = #[1, 5, 2, 3, 4]; tuple[1]; // 5 const filteredTuple = tuple.filter(num => num > 2); // #[5, 3, 4];
它们默认是深度不可变的:
const record = #{a: 1, b: 2}; record.b = 3; // 抛出 TypeError
它们可以被视为复合基本类型,并且可以通过值进行比较。
非常重要:两个深度相等的Record
将始终使用 ===
运算符返回 true
。
{a: 1, b: [3, 4]} === {a: 1, b: [3, 4]} // 使用对象 => false #{a: 1, b: #[3, 4]} === #{a: 1, b: #[3, 4]} // 使用记录 => true
我们可以认为Record
的变量就是其实际值,类似于常规JS原始类型。
它们与JSON
互操作:
const record = JSON.parseImmutable('{a: 1, b: [2, 3]}'); // #{a: 1, b: #[2, 3]} JSON.stringify(record); // '{a: 1, b: [2, 3]}'
它们只能包含其他Record
和Tuple
,或简单数据类型。
const record1 = #{ a: { regular: 'object' }, }; // 抛出 TypeError,因为记录不能包含对象 const record2 = #{ b: new Date(), }; // 抛出 TypeError,因为记录不能包含日期 const record3 = #{ c: new MyClass(), }; // 抛出 TypeError,因为记录不能包含类 const record4 = #{ d: function () { alert('forbidden'); }, }; // 抛出 TypeError,因为记录不能包含函数
2. 问题复现
上面提到了 -Memo很容易被破坏
简而言之:当React
渲染一个组件树时,它会从上往下渲染所有子组件。一旦渲染开始,我们就没有办法停止它。通常情况下,这是一件好事,因为渲染确保我们在屏幕上看到正确的状态反映。此外,渲染通常是快速的。
当然还有那些特殊情况
,它们需要处理一下耗时任务,从而使的渲染变得步履蹒跚。同时,由于某些原因,我们都有一些组件(前任留下的💩⛰️,或者核心业务
),我们无法轻易改变它们,与此同时它们的渲染速度还不尽人意。而此时,小可爱产品,又提出了优化需求。而我们就不得不赶鸭子上架。
幸运的是,React
内置机制中存在优化策略,那就是
在渲染时候,当它发现此次需要渲染的东西和之前是相同的,它是选择使用之前的结果。
假设,我们有如下的组件。
import { useState } from 'react'; export default function App() { let [color, setColor] = useState('red'); return ( <div> // 触发 <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, 789!</p> <ExpensiveComponent /> </div> ); } function ExpensiveComponent() { let now = performance.now(); while (performance.now() - now < 100) { // 手动模拟,耗时任务 -- 此处会卡顿100ms } // 打印被渲染的次数 console.log('我被渲染了'); return <p>耗时渲染</p>; }
我们可以将上面的代码,放置在任何线上环境进行测试。运行后我们就会发现,当App
中的color
变化时,会重新渲染一次被我们人为延缓渲染的<ExpensiveComponent />
组件。
在实际开发中,如果ExpensiveComponent
渲染需要很长时间,那这个部分就会很引起性能崩塌。
这是我们之前写的关于如何测试浏览器性能的文章,然后大家可以按需获取。
下面,我们就来解决上面出现的问题。
解法 1: 下放State
如果我们仔细看一下上面的问题代码
,我们会注意到返回的组件树
中只有一部分真正关心当前的color
。而<ExpensiveComponent/>
却对这些信息充耳不闻。
import { useState } from 'react'; export default function App() { let [color, setColor] = useState('red'); return ( <div> // 触发 <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, 789!</p> <ExpensiveComponent /> </div> ); } function ExpensiveComponent() { let now = performance.now(); while (performance.now() - now < 100) { // 手动模拟,耗时任务 -- 此处会卡顿100ms } // 打印被渲染的次数 console.log('我被渲染了'); return <p>耗时渲染</p>; }
我们把关心color
部分提取到Form
组件中然后将state
移动到该组件里:
export default function App() { return ( <> + <Form /> <ExpensiveComponent /> </> ); } function Form() { + let [color, setColor] = useState('red'); return ( <> + <input value={color} onChange={(e) => setColor(e.target.value)} /> + <p style={{ color }}>Hello, 789!</p> </> ); }
如果color
变化了,只有Form
会重新渲染。将state
下放到真正关心的组件中,这样就可以完美避开渲染污染。
动物世界看过哇。我们可以认为,这个是生殖隔离。虽然,有马和驴生下骡子的特例,但是骡子无法生育,就算存在污染,那也是限制在有限范围内。而不会出现子子孙孙无穷匮也的情况。