今天,我们发布了该材料的译文,其作者在分析了JavaScript中使用对象的功能之后,为React开发人员提供了一种加速应用程序的方法。 特别是,我们谈论的是这样一个事实,即变量,正如他们所说的那样,是“分配了一个对象”,并且通常简称为“对象”,实际上并不存储对象本身,而是存储对象的链接。 JavaScript中的函数也是对象,因此上面的内容对它们适用。 牢记这一点,设计React组件并严格分析其代码可以改善其内部机制并提高应用程序性能。
使用JavaScript处理对象的功能
如果您创建了两个看起来完全相同的函数并尝试进行比较,那么从系统的角度来看,它们是不同的。 为了验证这一点,您可以执行以下代码:
const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo;
现在,让我们尝试将一个变量分配给已经分配给另一个变量的现有函数,并比较这两个变量:
const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour;
如您所见,使用这种方法,严格相等运算符返回
true
。
对象自然具有相同的行为:
const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true
这里我们谈论的是JavaScript,但是如果您有使用其他语言进行开发的经验,那么您可能会熟悉指针的概念。 在上面的代码中,每次创建对象时,都会为其分配一部分系统内存。 当我们使用形式为
object1 = {}
的命令时,这将导致为某些数据填充专门为
object1
分配的内存。
很有可能将
object1
想象为与对象相关的数据结构位于内存中的地址。 命令
object2 = {}
的执行导致分配另一个专门为
object2
设计的存储区。
obect1
和
object2
是否
object2
同一存储区中? 不,他们每个人都有自己的情节。 这就是为什么当我们尝试比较
object1
和
object2
会得到
false
。 这些对象可能具有相同的结构,但是它们所在的内存中的地址不同,并且在比较过程中将检查这些地址。
通过执行命令
object3 = object1
,我们将
object1
的地址写入
object3
常量。 这不是一个新对象。 为该常数分配现有对象的地址。 您可以通过以下方式对此进行验证:
const object1 = { x: true }; const object3 = object1; object3.x = false; object1.x;
在此示例中,将在内存中创建一个对象,并将其地址写入常量
object1
。 然后,将相同的地址写入常量
object3
。 更改
object3
更改内存中的对象。 这意味着当使用对象的任何其他引用(例如,存储在
object1
的对象)访问对象时,我们已经可以使用其修改版本。
函数,对象和React
新手开发人员对上述机制的误解通常会导致错误,并且也许值得一提的是,考虑使用对象的功能。 但是,今天我们的主题是React应用程序的性能。 在这个领域中,即使是经验丰富的开发人员也可能会犯错误,他们根本不会关注JavaScript变量和常量不存储在对象本身中,而只是链接到它们的事实,从而影响React应用程序。
这与React有什么关系? React具有节省系统资源的智能机制,旨在改善应用程序性能:如果组件的属性和状态不变,则
render
功能不变。 显然,如果组件保持不变,则无需再次渲染。 如果没有任何变化,则
render
函数将返回与以前相同的值,因此无需执行它。 这种机制使React快速。 仅在必要时显示某些内容。
React使用标准的JavaScript功能检查组件的属性和状态是否相等,也就是说,它仅使用
==
运算符比较它们。 React不会对对象执行“浅”或“深”比较以确定它们的相等性。 “浅比较”是一种概念,用于描述对象的每个键值对的比较,而不是只比较内存中对象的地址(对它们的引用)的比较。 对象的“深度”比较甚至进一步进行,并且如果对象的比较属性的值也是对象,则它们还将比较这些对象的键值对。 对嵌套在其他对象中的所有对象重复此过程。 React不会做任何事情,只是检查链接是否相等。
例如,如果您将由
{ x: 1 }
形式的对象表示的组件的属性更改为看上去完全相同的另一个对象,React将重新渲染该组件,因为这些对象位于不同的内存区域中。 如果您还记得上面的示例,则在将组件的属性从
object1
更改为
object3
,React不会重新渲染该组件,因为常量
object1
和
object3
引用相同的对象。
使用JavaScript中的函数的工作方式完全相同。 如果React遇到地址不同的相同功能,它将重新渲染。 如果“新功能”只是指向已使用功能的链接,则不会重新渲染。
使用组件时的典型问题
这是使用组件的一种情况,不幸的是,在检查其他人的代码时会不断遇到我:
class SomeComponent extends React.PureComponent { get instructions() { if (this.props.do) { return 'Click the button: '; } return 'Do NOT click the button: '; } render() { return ( <div> {this.instructions} <Button onClick={() => alert('!')} /> </div> ); } }
在我们面前是一个非常简单的组成部分。 它是一个按钮,单击后将显示通知。 在按钮旁边显示使用说明,告知用户是否应按下该按钮。 通过设置
SomeComponent
的
do
(
do={true}
或
do={false}
)
SomeComponent
。
每次重新渲染
SomeComponent
组件时(当
do
属性的值从
true
更改为
false
,反之亦然),还将渲染
Button
元素。 尽管
onClick
处理程序始终是相同的,但每次调用
render
函数时都会重新创建。 结果,事实证明,每次在内存中显示该组件时,都会创建一个新函数,因为它的创建是在
render
函数中执行的,因此指向内存中新地址的链接会传递给
<Button />
,并且即使在一切都没有改变。
让我们来谈谈如何解决它。
解决问题
如果函数独立于组件(在
this
上下文中),则可以在组件外部定义它。 该组件的所有实例将使用相同的函数引用,因为在所有情况下,它将是相同的函数。 看起来是这样的:
const createAlertBox = () => alert('!'); class SomeComponent extends React.PureComponent { get instructions() { if (this.props.do) { return 'Click the button: '; } return 'Do NOT click the button: '; } render() { return ( <div> {this.instructions} <Button onClick={createAlertBox} /> </div> ); } }
与前面的示例不同,每次调用
render
createAlertBox
都将包含指向内存中相同区域的相同链接。 结果,将不会执行
Button
重复输出。
尽管
Button
组件很小并且可以快速呈现,但是与函数内部声明相关的上述问题也可以在大型,复杂的组件中发现,这些组件需要花费很多时间来呈现。 这会大大降低React应用程序的速度。 在这方面,遵循建议是有意义的,根据该建议,切勿在
render
方法内声明此类函数。
如果函数依赖于组件,即无法在组件外部定义,则可以将组件方法作为事件处理程序传递:
class SomeComponent extends React.PureComponent { createAlertBox = () => { alert(this.props.message); }; get instructions() { if (this.props.do) { return 'Click the button: '; } return 'Do NOT click the button: '; } render() { return ( <div> {this.instructions} <Button onClick={this.createAlertBox} /> </div> ); } }
在这种情况下,在
SomeComponent
每个实例中
SomeComponent
当您单击按钮时,将显示各种消息。
Button
元素的事件处理程序对于
SomeComponent
必须是唯一的。 传递
cteateAlertBox
方法时,
SomeComponent
重新渲染
SomeComponent
。
message
属性是否已更改都没有关系。
createAlertBox
函数的地址不会更改,这意味着不应再次呈现
Button
元素。 因此,您可以节省系统资源并提高应用程序的渲染速度。
这一切都很好。 但是,如果函数是动态的呢?
解决更复杂的问题
本材料的作者要求您注意以下事实:他准备了本节中的示例,并想到了第一件事,它适合于说明功能的重用。 这些示例旨在帮助读者理解该思想的实质。 尽管建议阅读本部分以了解正在发生的事情的本质,但作者建议您注意对原始文章的评论,因为一些读者建议了此处讨论的机制的更好版本,其中考虑了React内置的缓存失效和内存管理机制的功能。因此,在一个组件中有很多独特的动态事件处理程序是非常普遍的,例如,在代码中可以看到类似的东西,其中在
render
方法中使用
map
数组方法:
class SomeComponent extends React.PureComponent { render() { return ( <ul> {this.props.list.map(listItem => <li key={listItem.text}> <Button onClick={() => alert(listItem.text)} /> </li> )} </ul> ); } }
在这里,将显示不同数量的按钮,并且将创建不同数量的事件处理程序,每个事件处理程序都由一个唯一的函数表示,并且预先创建
SomeComponent
,尚不清楚这些函数将是什么。 如何解决这个难题?
在这里,备忘录将帮助我们,或更简单地说,是缓存。 对于每个唯一值,创建一个函数并将其放入缓存中。 如果此唯一值再次出现,则足以从缓存中获取之前已放置在缓存中的与其对应的功能。
这是该想法的实现形式:
class SomeComponent extends React.PureComponent {
数组的每个元素都由
getClickHandler
方法处理。 第一次使用某个值调用此方法时,它将创建一个对该值唯一的函数,将其放入缓存中并返回。 对该方法的所有后续调用,将相同的值传递给它,将导致该方法仅从缓存返回指向该函数的链接。
结果,重新渲染
SomeComponent
不会重新渲染
Button
。 同样,将元素添加到
list
属性将为每个按钮动态创建事件处理程序。
如果处理程序是由多个变量定义的,则需要为处理程序创建唯一标识符,但要比通过
map
方法获得的每个JSX对象通常创建唯一
key
属性的过程复杂得多。
在这里,我想警告您使用数组索引作为标识符的可能问题。 事实是,使用这种方法,如果数组中元素的顺序更改或删除了其中某些元素,则可能会遇到错误。 因此,例如,如果起初类似的数组看起来像
[ 'soda', 'pizza' ]
,然后变成
[ 'pizza' ]
,并且您使用
listeners[0] = () => alert('soda')
形式的命令来缓存事件处理程序
listeners[0] = () => alert('soda')
,您将发现,当用户单击分配了标识符为0的处理程序且该按钮根据
[ 'pizza' ]
数组的内容应显示
pizza
消息的按钮时,将显示一个
soda
消息。 由于相同的原因,不建议将数组索引用作键属性。
总结
在本文中,我们研究了内部JavaScript机制的功能,并考虑了可以加快React应用程序渲染的速度。 我们希望这里提出的想法会派上用场。
亲爱的读者们! 如果您知道优化React应用程序的任何有趣方式,请分享它们。
