理解React中的渲染行为
随着生活、死亡、命运和税收一样,React的渲染行为也是生活中最伟大的真理和谜团之一。
让我们深入探讨吧!
和其他人一样,我也是从jQuery开始前端开发之旅的。当时基于纯JS的DOM操作非常困难,所以这是每个人都在做的事情。然后渐渐地,JavaScript-based frameworks变得如此突出,以至于我不能再忽视它们了。
我学习的第一个是Vue。我遇到了很大的困难,因为组件、状态和其他一切都是一种全新的心智模型,把所有东西都放进去是一种很大的痛苦。但最终,我做到了,并且为自己鼓掌。恭喜你,伙计,我告诉自己,你已经完成了陡峭的攀登;现在,如果你需要学习其他框架,它们将会非常容易。
所以,有一天,当我开始使用learning React时,我意识到我之前的判断是多么错误。Facebook通过引入Hooks并告诉每个人“嘿,从现在开始使用这个。但不要重写类;类没问题。实际上,也不完全是这样,但没关系。但是Hooks是一切,它们是未来。
明白了吗?太好了!”。
最终,我也跨过了那座山。但是,接下来我遇到了一个和React本身一样重要和困难的东西:渲染。
如果你在React中遇到过渲染及其神秘之处,你就知道我在说什么。如果你还没有遇到过,你对即将发生的事情一无所知! 😂
但是,在浪费时间之前,一个好习惯是问一下你会从中获得什么(与我不同,我是一个过度兴奋的白痴,会为了学习而学习 😭😭)。如果你作为React开发者的生活没有担心渲染这个问题,为什么要在意呢?这是个好问题,所以我们先回答这个问题,然后再看看渲染到底是什么。
为什么理解React的渲染行为很重要?
我们开始学习React时,通过编写(现在是函数式的)组件来返回一些被称为JSX的东西。我们也理解这个JSX会以某种方式转换为实际的HTML DOM元素,显示在页面上。页面随着状态的更新而更新,路由如预期般改变,一切都很好。但是这种对React工作原理的理解是幼稚的,并且是许多问题的根源。
虽然我们经常成功地编写了基于React的完整应用程序,但有时我们会发现应用程序的某些部分(或整个应用程序)非常慢。而最糟糕的是…我们完全没有任何线索为什么!我们做了一切正确的事情,没有看到任何错误或警告,我们遵循了组件设计、编码标准等所有良好的实践,而没有网络延迟或昂贵的业务逻辑计算在幕后运行。🤔
有时,问题完全不同:性能没有问题,但应用程序的行为很奇怪。例如,对认证后端进行三次API调用,但对其他所有调用只进行一次。或者某些页面被重绘两次,同一页面的两次渲染之间的可见过渡创建了令人不舒服的用户体验。
最糟糕的是,在这些情况下没有外部帮助可用。如果你去你最喜欢的开发者论坛上问这个问题,他们会回答:“不看看你的应用程序,无法得知。你能把一个最小工作示例附在这里吗?”当然,出于法律原因,你不能附上整个应用程序,而那部分的一个微小的工作示例可能不包含该问题,因为它没有与整个系统交互,就像它在实际应用程序中那样。
完全绝望了?是的,如果你问我的话。🤭🤭
所以,除非你想看到这样的苦痛之日,我建议你开发一种理解 – 并且兴趣,我必须坚持; 不情愿获得的理解在React世界中不会让你走得太远 – 这个被称为在React中的渲染的被误解的东西。相信我,理解它并不难,虽然很难掌握,但你不需要了解每个细节就能走得很远。
在React中渲染意味着什么?
那,我的朋友,是一个很好的问题。当我们学习React时,我们不倾向于提出这个问题(我知道,因为我没有提出),因为“渲染”这个词也许使我们产生一种误以为熟悉的错觉。尽管其字典意义完全不同(并且在这个讨论中并不重要),但我们程序员已经有了一个关于它应该意味着什么的想法。与屏幕、3D API、图形卡和阅读产品规范一起工作训练我们的思维,当我们读到“渲染”这个词时,我们会想到“绘画一幅画”的想法。在游戏引擎编程中,有一个渲染器,其唯一的工作就是——确切地说!根据场景绘制世界。
因此,我们认为当React“渲染”某些内容时,它会收集所有组件并重新绘制Web页面的DOM。但在React世界中(是的,即使在官方文档中),这不是渲染的意义。因此,让我们系好安全带,深入了解一下React内部。
你一定听说过React维护着一个虚拟DOM,并定期将其与实际DOM进行比较,并根据需要应用更改(这就是为什么你不能简单地将jQuery和React放在一起使用 – React需要完全控制DOM)。现在,这个虚拟DOM不是由HTML元素组成的,而是由React元素组成的。有什么区别?好问题!为什么不创建一个小型的React应用程序,亲自看看呢?
我为此目的创建了一个非常简单的React应用程序。整个代码只是一个包含几行的单个文件:
import React from "react";
import "./styles.css";
export default function App() {
const element = (
Hello, there!
Let's take a look inside React elements
);
console.log(element);
return element;
}
注意我们在这里做了什么?
是的,只是记录JSX元素的样子。这些JSX表达式和组件我们已经写了很多次,但我们很少注意到发生了什么。如果你打开浏览器的开发控制台并运行这个应用程序,你会看到一个展开为Object
的结果:
这可能看起来令人生畏,但请注意一些有趣的细节:
- 我们看到的是一个普通的JavaScript对象,而不是一个DOM节点。
- 注意,属性
props
表示它有一个className
为App
的属性(这是代码中设置的CSS类),并且该元素有两个子元素(这也匹配,子元素是
和
_source
属性告诉我们元素的主体在源代码中的位置。正如你所看到的,它将文件名命名为App.js
作为源代码,并提到第6行。如果你再次查看代码,你会发现第6行恰好在开头的JSX标签之后,这是有道理的。JSX括号包含了React元素;它们不是它的一部分,因为它们在稍后会转换为React.createElement()
调用。__proto__
属性告诉我们,这个对象的所有属性都是从根JavaScriptObject
派生的,再次强调了我们所看到的只是日常JavaScript对象。
所以,现在,我们明白了所谓的虚拟DOM看起来并不像真正的DOM,而是表示UI在那个时间点的React(JavaScript)对象树。
筋疲力尽了吗?
相信我,我也是。🙂我反复思考这些想法,试图以最好的方式呈现它们,然后想出合适的词语来表达它们并重新排列它们——这并不容易。 😫
但我们被分散了注意力!
在走了这么远之后,我们现在有能力回答我们追寻的问题:React中的渲染是什么?
嗯,渲染是React引擎遍历虚拟DOM并收集当前状态、props、结构、UI中的期望变化等的过程。React现在使用一些计算来更新虚拟DOM,并将新结果与页面上的实际DOM进行比较。这个计算和比较是React团队正式称之为“协调”的过程,如果你对他们的想法和相关算法感兴趣,可以查看官方的docs。
是时候提交了!
渲染部分完成后,React开始一个名为“提交”的阶段,在此阶段它将必要的更改应用于DOM。这些更改是同步应用的(一个接一个地应用,尽管预计很快会有一种并发工作的新模式),并更新DOM。React何时以及如何应用这些更改不是我们关心的问题,因为这是完全在幕后进行的,而且随着React团队尝试新事物,这种情况可能会不断变化。
React应用中的渲染和性能
到目前为止,我们已经理解了渲染意味着收集信息,并且它不必每次都导致可视DOM的更改。我们也知道我们认为的“渲染”是一个涉及渲染和提交的两步过程。现在我们将看到在React应用中如何触发渲染(更重要的是,重新渲染)以及不知道细节可能导致应用性能不佳。
由于父组件的更改而重新渲染
如果React中的父组件发生更改(比如,因为其状态或props发生了变化),React将遍历整个树下的这个父元素并重新渲染所有组件。如果你的应用程序有许多嵌套组件和许多互动,那么每次更改父组件时,你都会不知不觉地承受巨大的性能损耗(假设你只想更改父组件)。
是的,渲染不会导致React更改实际的DOM,因为在协调过程中,它会检测到这些组件没有发生变化。但是,这仍然是CPU时间和内存的浪费,你会惊讶地发现它会迅速累积起来。
由于上下文的更改而重新渲染
React的上下文功能似乎是每个人最喜欢的状态管理工具(实际上它根本不是为此而构建的)。这一切都非常方便——只需在上层组件中包装上下文提供者,剩下的就是简单的事情!大多数React应用程序都是这样构建的,但如果你到目前为止已经读完了这篇文章,你可能已经发现了问题所在。是的,每次更新上下文对象时,它会触发所有树组件的大规模重新渲染。
大多数应用程序没有性能意识,所以没有人注意到,但如前所述,在高负载、高互动的应用程序中,这种疏忽可能代价高昂。
改善React渲染性能
那么,鉴于这一切,我们该怎么做来改善我们应用程序的性能?事实证明,我们可以做一些事情,但请注意,我们只会在功能组件的上下文中讨论。React团队强烈不推荐使用基于类的组件,并且它们正在逐渐淘汰。
使用Redux或类似的库进行状态管理
喜欢Context快速而简洁的世界的人往往讨厌Redux,但这个东西非常受欢迎,原因之一就是性能——Redux中的connect()
函数几乎总能正确地仅渲染所需的组件。是的,只需遵循标准的Redux架构,性能就能免费获得。如果您采用Redux架构,避免大部分性能(和其他)问题绝非夸大其词。
使用memo()
“冻结”组件
“memo”一词来自于记忆化,这是缓存的一种花哨名称。如果您没有接触过缓存,没关系;这是一个简化的描述:每当您需要某些计算/操作结果时,您会查找您维护先前结果的位置;如果找到了,直接返回该结果;如果没有找到,继续执行该操作/计算。
在直接进入memo()
之前,让我们先看看React中无必要的渲染是如何发生的。我们从一个简单的场景开始:应用程序UI的一个小部分,用于显示用户喜欢服务/产品的次数(如果您对使用情况感到困惑,考虑一下在Medium上您可以多次“拍手”以显示您对一篇文章的支持/喜欢程度)。
还有一个按钮,允许用户将点赞数增加1。最后,还有一个显示用户基本帐户详细信息的组件。如果您对此感到困惑,不要担心;我现在将逐步为所有内容提供代码(而且并不多),最后还有一个链接,您可以在其中玩弄工作中的应用程序并提高您的理解能力。
让我们先处理有关客户信息的组件。让我们创建一个名为CustomerInfo.js
的文件,其中包含以下代码:
import React from "react";
export const CustomerInfo = () => {
console.log("CustomerInfo was rendered! :O");
return (
Name: Sam Punia
Email: [email protected]
Preferred method: Online
);
};
没什么花哨的吧?
只是一些信息文本(也可以通过props传递),不会随着用户与应用程序的交互而改变(对于那些纯粹主义者来说,是的,当然它可以改变,但关键是,与应用程序的其余部分相比,它几乎是静态的)。但请注意console.log()
语句。这将是我们知道组件是否已渲染的线索(请记住,“已渲染”意味着已收集其信息并进行了计算/比较,并不表示它已绘制到实际DOM上)。
因此,在我们的测试中,如果在浏览器控制台中看不到此类消息,表示根本未渲染我们的组件;如果看到它出现10次,表示该组件已渲染10次;依此类推。
现在让我们看看我们的主组件如何使用此客户信息组件:
import React, { useState } from "react";
import "./styles.css";
import { CustomerInfo } from "./CustomerInfo";
export default function App() {
const [totalLikes, setTotalLikes] = useState(0);
return (
您迄今为止已经点赞了我们{totalLikes}次。
);
}
所以,我们看到App
组件具有一个通过useState()
hook进行内部状态管理的组件。该状态计算用户点赞服务/网站的次数,并且最初设置为零。就React应用程序而言,没有什么具有挑战性的,对吧?在UI方面,情况如下:
这个按钮看起来太诱人了,至少对我来说!但在这之前,我会打开浏览器的开发控制台并清除它。然后,我会点击这个按钮几次,这是我看到的:
我点击了这个按钮19次,正如预期的那样,点赞总数是19。由于配色方案使得很难阅读,所以我添加了一个红色框来突出主要内容:组件被渲染了20次!
为什么是20次?
一次是在最初渲染时,然后是在按钮被点击时19次。按钮改变了totalLikes
,它是组件内部的一个状态值,结果导致主组件重新渲染。正如我们在本文早期部分学到的那样,其中的所有组件也会重新渲染。这是不希望的,因为
组件在此过程中没有发生变化,但仍然参与了渲染过程。
我们如何防止这种情况发生?
就像本节标题所说,使用memo()
函数创建一个“保留”或缓存的组件副本。使用记忆化组件,React会检查其props并将其与之前的props进行比较,如果没有变化,React就不会从该组件提取新的“渲染”输出。
让我们在CustomerInfo.js
文件中添加这行代码:
export const MemoizedCustomerInfo = React.memo(CustomerInfo);
是的,这就是我们需要做的全部!现在是时候在我们的主组件中使用它并查看是否有任何变化:
import React, { useState } from "react";
import "./styles.css";
import { MemoizedCustomerInfo } from "./CustomerInfo";
export default function App() {
const [totalLikes, setTotalLikes] = useState(0);
return (
到目前为止,您已经给我们点赞{totalLikes}次。
);
}
是的,只有两行代码发生了变化,但我想总体展示整个组件。在UI上没有任何变化,所以如果我使用新版本并点击点赞按钮几次,我会得到这个结果:
那么,我们有多少个控制台消息?
只有一个!这意味着除了最初的渲染外,组件没有被修改过。想象一下在一个真正高规模的应用程序中的性能提升!好吧,好吧,我承诺的代码播放链接是here。要复制之前的示例,您需要导入并使用CustomerInfo
而不是来自CustomerInfo.js
的MemoizedCustomerInfo
。
也就是说,memo()
并不是您可以到处使用的神奇魔法,期望它能带来神奇的结果。过度使用memo()
可能会在应用程序中引入棘手的bug,并有时会导致某些预期的更新失败。有关“过早”优化的一般建议在这里也适用。首先,按照您的直觉构建应用程序;然后,进行一些密集的分析以查看哪些部分较慢,如果似乎记忆化组件是正确的解决方案,那么只需在这时引入它。
“智能”组件设计
我在引号中放置了“智能”一词,因为:1)智能是高度主观和情境性的;2)所谓的智能行为经常会带来不愉快的后果。因此,我对本节的建议是:不要对自己的行为过于自信。
解决了这个问题后,提升渲染性能的方法之一是设计和放置组件的方式有所不同。例如,可以将子组件重构并移动到层级结构的其他位置,以避免重新渲染。没有规定说“ChatPhotoView组件必须始终在Chat组件内”。在特殊情况下(这些情况是我们有数据支持证明性能受到影响的情况下),弯曲/打破规则实际上可能是一个很好的主意。
结论
在一般情况下,可以做更多的优化,但由于本文是关于渲染的,所以我限制了讨论的范围。不管怎样,我希望现在你对React在幕后发生的事情有了更好的了解,知道了什么是渲染以及它如何影响应用程序的性能。
接下来,让我们了解一下?