事件处理程序缓存和React应用程序性能改进

今天,我们发布了该材料的译文,其作者在分析了JavaScript中使用对象的功能之后,为React开发人员提供了一种加速应用程序的方法。 特别是,我们谈论的是这样一个事实,即变量,正如他们所说的那样,是“分配了一个对象”,并且通常简称为“对象”,实际上并不存储对象本身,而是存储对象的链接。 JavaScript中的函数也是对象,因此上面的内容对它们适用。 牢记这一点,设计React组件并严格分析其代码可以改善其内部机制并提高应用程序性能。



使用JavaScript处理对象的功能


如果您创建了两个看起来完全相同的函数并尝试进行比较,那么从系统的角度来看,它们是不同的。 为了验证这一点,您可以执行以下代码:

const functionOne = function() { alert('Hello world!'); }; const functionTwo = function() { alert('Hello world!'); }; functionOne === functionTwo; // false 

现在,让我们尝试将一个变量分配给已经分配给另一个变量的现有函数,并比较这两个变量:

 const functionThree = function() { alert('Hello world!'); }; const functionFour = functionThree; functionThree === functionFour; // true 

如您所见,使用这种方法,严格相等运算符返回true
对象自然具有相同的行为:

 const object1 = {}; const object2 = {}; const object3 = object1; object1 === object2; // false object1 === object3; // true 

这里我们谈论的是JavaScript,但是如果您有使用其他语言进行开发的经验,那么您可能会熟悉指针的概念。 在上面的代码中,每次创建对象时,都会为其分配一部分系统内存。 当我们使用形式为object1 = {}的命令时,这将导致为某些数据填充专门为object1分配的内存。

很有可能将object1想象为与对象相关的数据结构位于内存中的地址。 命令object2 = {}的执行导致分配另一个专门为object2设计的存储区。 obect1object2是否object2同一存储区中? 不,他们每个人都有自己的情节。 这就是为什么当我们尝试比较object1object2会得到false 。 这些对象可能具有相同的结构,但是它们所在的内存中的地址不同,并且在比较过程中将检查这些地址。

通过执行命令object3 = object1 ,我们将object1的地址写入object3常量。 这不是一个新对象。 为该常数分配现有对象的地址。 您可以通过以下方式对此进行验证:

 const object1 = { x: true }; const object3 = object1; object3.x = false; object1.x; // false 

在此示例中,将在内存中创建一个对象,并将其地址写入常量object1 。 然后,将相同的地址写入常量object3 。 更改object3更改内存中的对象。 这意味着当使用对象的任何其他引用(例如,存储在object1的对象)访问对象时,我们已经可以使用其修改版本。

函数,对象和React


新手开发人员对上述机制的误解通常会导致错误,并且也许值得一提的是,考虑使用对象的功能。 但是,今天我们的主题是React应用程序的性能。 在这个领域中,即使是经验丰富的开发人员也可能会犯错误,他们根本不会关注JavaScript变量和常量不存储在对象本身中,而只是链接到它们的事实,从而影响React应用程序。

这与React有什么关系? React具有节省系统资源的智能机制,旨在改善应用程序性能:如果组件的属性和状态不变,则render功能不变。 显然,如果组件保持不变,则无需再次渲染。 如果没有任何变化,则render函数将返回与以前相同的值,因此无需执行它。 这种机制使React快速。 仅在必要时显示某些内容。

React使用标准的JavaScript功能检查组件的属性和状态是否相等,也就是说,它仅使用==运算符比较它们。 React不会对对象执行“浅”或“深”比较以确定它们的相等性。 “浅比较”是一种概念,用于描述对象的每个键值对的比较,而不是只比较内存中对象的地址(对它们的引用)的比较。 对象的“深度”比较甚至进一步进行,并且如果对象的比较属性的值也是对象,则它们还将比较这些对象的键值对。 对嵌套在其他对象中的所有对象重复此过程。 React不会做任何事情,只是检查链接是否相等。

例如,如果您将由{ x: 1 }形式的对象表示的组件的属性更改为看上去完全相同的另一个对象,React将重新渲染该组件,因为这些对象位于不同的内存区域中。 如果您还记得上面的示例,则在将组件的属性从object1更改为object3 ,React不会重新渲染该组件,因为常量object1object3引用相同的对象。

使用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>   ); } } 

在我们面前是一个非常简单的组成部分。 它是一个按钮,单击后将显示通知。 在按钮旁边显示使用说明,告知用户是否应按下该按钮。 通过设置SomeComponentdodo={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重新渲染SomeComponentmessage属性是否已更改都没有关系。 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 { //    SomeComponent        //   . clickHandlers = {}; //       //    . getClickHandler(key) {   //       ,  .   if (!Object.prototype.hasOwnProperty.call(this.clickHandlers, key)) {     this.clickHandlers[key] = () => alert(key);   }   return this.clickHandlers[key]; } render() {   return (     <ul>       {this.props.list.map(listItem =>         <li key={listItem.text}>           <Button onClick={this.getClickHandler(listItem.text)} />         </li>       )}     </ul>   ); } } 

数组的每个元素都由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应用程序的任何有趣方式,请分享它们。

Source: https://habr.com/ru/post/zh-CN426053/


All Articles