功能性React组件与基于类的组件有何不同?

功能性React组件与基于类的组件有何不同? 在相当长的一段时间内,对这个问题的传统答案是:“类的使用允许您使用组件的大量功能,例如状态。” 现在,随着钩子的出现,这个答案不再反映真实的情况。

您可能已经听说这些类型的组件中的一种具有比另一种更好的性能。 但是哪一个呢? 多数测试此基准的基准都有缺陷 ,因此我将非常谨慎地根据其结果得出结论 。 性能主要取决于代码中发生的情况,而不取决于是否选择功能组件或基于类的组件来实现某些功能。 我们的研究表明,不同类型的组件之间的性能差异可以忽略不计。 但是,应注意,用于它们的优化策略略有不同



无论如何,如果没有充分的理由,并且不介意比其他人先使用这些技术的人,我不建议您使用新技术重写现有组件。 Hook仍然是一项新技术(与2014年的React库相同),并且React手册中尚未包括一些适用于它们的“最佳实践”。

我们终于到哪里去了? React的功能组件和基于类的组件之间是否有根本的区别? 当然也有这样的差异。 这些是使用此类组件的心理模型的差异。 在本文中,我将考虑它们之间最严重的区别。 自从2015年出现功能组件以来,它就已经存在,但是它经常被忽略。 它包含以下事实:功能组件捕获呈现的值。 让我们谈谈这到底意味着什么。

应当指出,该材料并不构成评估不同类型组件的尝试。 我只是描述了React中两种编程模型之间的区别。 如果您想根据创新了解更多有关功能组件的用法,请参阅挂钩问题列表。

基于功能和类的组件代码的特征是什么?


考虑以下组件:

function ProfilePage(props) {  const showMessage = () => {    alert('Followed ' + props.user);  };  const handleClick = () => {    setTimeout(showMessage, 3000);  };  return (    <button onClick={handleClick}>Follow</button>  ); } 

它显示一个按钮,该按钮通过按下setTimeout函数来模拟网络请求,然后显示一个确认操作的消息框。 例如,如果' props.user 'Dan'存储在props.user ,则在消息窗口中,三秒钟后,将显示'Followed Dan' props.user 'Followed Dan'

请注意,此处是否使用箭头函数或函数声明都没有关系。 表单function handleClick()构造将以完全相同的方式工作。

如何将此组件重写为类? 如果仅重做刚刚检查过的代码,然后将其转换为基于类的组件代码,则会得到以下信息:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; handleClick = () => {   setTimeout(this.showMessage, 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

通常认为两个这样的代码片段是等效的。 在代码重构的过程中,开发人员通常是完全免费的,无需考虑可能的后果,就能将它们彼此转化。


这些代码似乎是等效的

但是,这些代码段之间略有不同。 仔细看看它们。 看到区别了吗? 例如,我没有立即看到她。

因此,对于那些想了解自己正在发生的事情的本质的人,我们将考虑这种差异,这是该代码的一个有效示例。

在继续之前,我想强调一下,所讨论的差异与React钩子无关。 顺便说一下,在前面的示例中,甚至没有使用钩子。 这是关于React中函数和类之间的区别。 而且,如果您打算在React应用程序中使用许多功能组件,那么您可能想了解这种区别。

实际上,我们将以React应用程序中经常遇到的错误为例来说明函数和类之间的差异。

在React应用程序中常见的错误。


打开示例页面该页面显示一个列表,您可以使用该列表选择用户配置文件,以及两个Profile按钮,分别由ProfilePageFunctionProfilePageClass (按功能以及基于类)显示,其代码如上所示。

对于这些按钮中的每一个,请尝试执行以下操作序列:

  1. 点击按钮。
  2. 单击按钮后3秒钟之前更改选定的配置文件。
  3. 阅读消息框中显示的文本。

完成此操作后,您将注意到以下功能:

  • 当您单击具有所选Dan配置文件的功能组件所形成的按钮,然后切换到Sophie配置文件时,消息框中将显示'Followed Dan'
  • 如果对由基于类的组件形成的按钮执行相同操作,将显示'Followed Sophie'


基于类的组件功能

在此示例中,功能组件的行为是正确的。 如果我订阅了某人的个人资料,然后又切换到另一个个人资料,则我的组件不应怀疑我已订阅了谁的个人资料。 显然,基于类使用的机制的实现包含一个错误(顺便说一句,您肯定应该成为Sofia的订阅者)。

基于类的组件故障的原因


为什么基于类的组件具有这种行为? 为了理解这一点,让我们看一下我们类中的showMessage方法:

 class ProfilePage extends React.Component { showMessage = () => {   alert('Followed ' + this.props.user); }; 

此方法从this.props.user读取数据。 React中的属性是不可变的,因此它们不会更改。 但是,与往常一样,这是一个可变实体。

实际上,将其放在课堂上的目的在于改变的能力。 React库本身会定期执行this更改,从而允许使用最新版本的render方法和组件生命周期方法。

结果,如果我们的组件在执行请求期间重新渲染,则this.props将更改。 之后, showMessage方法showMessage “太新”的props实体中读取user值。

这使您可以对用户界面进行有趣的观察。 如果从概念上说用户界面是应用程序当前状态的函数,则事件处理程序是呈现结果的一部分-就像可见的呈现结果一样。 我们的事件处理程序“属于”特定的渲染操作以及特定的属性和状态。

但是,安排超时this.props读取的超时会违反此连接。 showMessage showMessage未“绑定”到任何特定的呈现操作;因此,它“丢失”了正确的属性。 从中读取数据会中断此连接。

如何通过基于类的组件来解决问题?


想象一下,React中没有功能组件。 那么如何解决这个问题呢?

我们需要某种机制来“恢复”具有正确属性的render方法与showMessage showMessage之间的连接,后者从属性中读取数据。 该机制应位于丢失具有正确数据的props本质的位置。

一种方法是事先在事件处理程序中读取this.props ,然后将读取的内容显式传递给setTimeout使用的回调函数:

 class ProfilePage extends React.Component { showMessage = (user) => {   alert('Followed ' + user); }; handleClick = () => {   const {user} = this.props;   setTimeout(() => this.showMessage(user), 3000); }; render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

这种方法有效 。 但是,随着时间的流逝,这里使用的其他构造将导致代码量的增加,并导致错误的可能性增加。 如果我们需要一个以上的财产怎么办? 如果我们还需要与国家合作怎么办? 如果showMessage方法showMessage另一个方法,并且此方法读取this.props.somethingthis.state.something ,那么我们将再次面临相同的问题。 为了解决这个问题,我们必须将this.propsthis.state作为参数传递this.props this.state调用的所有方法。

如果确实如此,它将破坏使用基于类的组件所带来的所有便利。 您需要以这种方式使用方法的事实很难记住,也很难实现自动化,因此,开发人员经常(而不是使用类似的方法)同意他们的项目中存在错误。

同样,将alert代码嵌入handleClick不能解决更全局的问题。 我们需要对代码进行结构化,以便可以将其分为许多方法,而且还可以使我们读取与特定调用相关的呈现操作相对应的属性和状态。 顺便说一下,这个问题甚至不仅仅适用于React。 您可以在任何用于开发用户界面的库中播放该库,该库会将数据放入可变对象中,例如this

也许为了解决此问题,您可以在构造函数中将方法绑定this方法?

 class ProfilePage extends React.Component { constructor(props) {   super(props);   this.showMessage = this.showMessage.bind(this);   this.handleClick = this.handleClick.bind(this); } showMessage() {   alert('Followed ' + this.props.user); } handleClick() {   setTimeout(this.showMessage, 3000); } render() {   return <button onClick={this.handleClick}>Follow</button>; } } 

但这不能解决我们的问题。 请记住,这是因为我们从this.props中读取数据太晚了,而不是使用所使用的语法! 但是,如果我们依靠JavaScript闭包,则将解决此问题。

开发人员经常尝试避免闭包,因为考虑随着时间的推移无法改变的价值并不容易 。 但是React中的属性是不可变的! (或者至少强烈建议这样做)。 这使您可以停止将闭包视为某种东西,因此程序员可以如他们所说的那样“自“”。

这意味着,如果您“锁定”了闭包中特定渲染操作的属性或状态,则始终可以指望它们保持不变。

 class ProfilePage extends React.Component { render() {   //  !   const props = this.props;   //    ,      render.   //   -   .   const showMessage = () => {     alert('Followed ' + props.user);   };   const handleClick = () => {     setTimeout(showMessage, 3000);   };   return <button onClick={handleClick}>Follow</button>; } } 

如您所见,此处我们在调用render方法期间“捕获”了属性。


渲染调用捕获的属性

使用这种方法,可以保证render方法中的任何代码(包括showMessage )都可以看到在对该方法的特定调用期间捕获的属性。 结果,React将不再能够阻止我们做我们需要做的事情。

render方法中,您可以描述任意数量的辅助功能,并且所有这些功能都可以使用“捕获的”属性和状态。 这就是闭包如何解决我们的问题的方式。

使用闭包分析问题的解决方案


我们刚刚得到的结果使我们能够解决问题 ,但是这样的代码看起来很奇怪。 如果在render方法中声明了函数而不是在类方法中声明了,为什么根本需要一个类?

实际上,我们可以通过以包围它的类的形式摆脱“外壳”来简化此代码:

 function ProfilePage(props) { const showMessage = () => {   alert('Followed ' + props.user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

在这里,与前面的示例一样,属性是在函数中捕获的,因为React将它们作为参数传递给它。 与this不同的是,React永远不会props对象。

如果在函数声明props销毁了props这将变得更加明显:

 function ProfilePage({ user }) { const showMessage = () => {   alert('Followed ' + user); }; const handleClick = () => {   setTimeout(showMessage, 3000); }; return (   <button onClick={handleClick}>Follow</button> ); } 

当父组件使用其他属性ProfilePage时,React将ProfilePage调用ProfilePage函数。 但是已经被调用的事件处理程序属于该函数的上一次调用,该调用使用其自己的user值和其自身的showMessage showMessage (读取此值)。 所有这些都保持不变。

这就是为什么在我们的示例的原始版本中,当使用功能组件时,在显示消息之前单击相应按钮后选择另一个配置文件不会更改任何内容。 如果在单击按钮之前选择了Sophie个人资料,无论发生什么, 'Followed Sophie'将显示在消息窗口中。


使用功能组件

此行为是正确的(您可能还想注册Sunil )。

现在我们已经弄清楚React中的函数和类之间的最大区别是什么。 如前所述,我们正在谈论功能组件捕获值的事实。 现在让我们谈谈钩子。

钩子


使用挂钩时,“捕获值”的原理扩展到该状态。 考虑以下示例:

 function MessageThread() { const [message, setMessage] = useState(''); const showMessage = () => {   alert('You said: ' + message); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value); }; return (   <>     <input value={message} onChange={handleMessageChange} />     <button onClick={handleSendClick}>Send</button>   </> ); } 

在这里你可以尝试他

尽管这不是消息传递应用程序界面的示例,但该项目说明了相同的想法:如果用户发送了消息,则不应混淆该组件以发送哪个消息。 该功能组件的message常量捕获“属于”该组件的状态,该状态使浏览器获得对其调用的按钮的单击处理程序。 结果, message存储了单击“ Send按钮时输入字段中的内容。

通过功能组件捕获属性和状态的问题


我们知道,默认情况下,React中的功能组件会捕获属性和状态。 但是,如果我们需要从不属于特定函数调用的属性或状态中读取最新数据,该怎么办? 如果我们想“ 从未来阅读它们 ”怎么办?

在基于类的组件中,这可以简单地通过引用this.propsthis.state来完成,因为this是可变实体。 她的变化是参与React。 功能组件还可以使用所有组件共享的可变值。 这些值称为ref

 function MyComponent() { const ref = useRef(null); //     `ref.current`. // ... } 

但是,程序员需要独立管理这些值。

ref的本质与类的实例的字段起相同的作用 。 这是进入可变命令世界的“紧急出口”。 您可能熟悉DOM refs的概念,但是这种想法更为笼统。 可以将其与程序员可以放入东西的盒子进行比较。

甚至在外部,形式this.something的构造看起来也像something.current构造的镜像。 它们代表了相同的概念。

默认情况下,React不会在功能组件中为最新的属性或状态值创建ref实体。 在许多情况下,您将不需要它们,并且自动创建它们将浪费时间。 但是,如有必要,可以与他们一起组织活动:

 function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; const handleSendClick = () => {   setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => {   setMessage(e.target.value);   latestMessage.current = e.target.value; }; 

如果我们在showMessage阅读了该message ,那么在单击“ Send按钮时,我们将看到该字段中的消息。 但是,如果您阅读latestMessage.current ,则可以获得最新值-即使我们在单击“ Send按钮后继续在字段中输入文本。

您可以将此示例与示例进行比较,以独立评估差异。 ref的值是“避免”渲染均匀性的一种方式,在某些情况下它可能非常有用。

通常,应避免在渲染过程中读取或写入ref值,因为这些值是可变的。 我们努力使渲染可预测。 但是,如果我们需要获取存储在属性或状态中的某些事物的最新值,则手动更新ref值可能是一项繁琐的任务。 可以使用以下效果使其自动化:

 function MessageThread() { const [message, setMessage] = useState(''); //    . const latestMessage = useRef(''); useEffect(() => {   latestMessage.current = message; }); const showMessage = () => {   alert('You said: ' + latestMessage.current); }; 

这是使用此代码的示例

我们在效果内分配了一个值,因此, ref的值仅在DOM更新后才会更改。 这确保了我们的突变不会破坏依赖于渲染操作连续性的“ 时间切片”和“暂挂”等功能。

经常不需要以这种方式使用ref值。 捕获属性或状态通常看起来是标准系统行为的更好模式。 但是,在使用命令性API (例如使用间隔或订阅的命令性API)时,这可能很方便。 请记住,您可以使用任何值进行此操作-使用属性,状态中存储的变量,整个props对象甚至使用函数。

此外,此模式对于优化目的可能很有用。 例如,当诸如useCallback类的useCallback改变得太频繁时。 没错, 首选解决方案通常是使用减速器

总结


在本文中,我们研究了使用基于类的组件的错误模式之一,并讨论了如何使用闭包解决此问题。 但是,您可能会注意到,当您尝试通过指定依赖项数组来优化挂钩时,可能会遇到与过时的闭包有关的错误。 这是否意味着故障本身就是问题。 我不这么认为。

如上所示,闭包实际上可以帮助我们解决难以发现的小问题。 同样,它们使编写并行运行的代码变得更加容易。 这可能是由于以下事实:“内部”锁定了渲染组件的正确属性和状态。

到目前为止,在所有情况下,由于“功能不变”或“属性始终保持不变”的错误假设,出现了“过时的关闭”问题。 我希望阅读完这些材料后,您可以确信事实并非如此。

函数“捕获”它们的属性和状态-因此了解哪些函数是重要的。 这不是一个错误;它是功能组件的功能。 例如,不应将功能从useEffectuseCalback的“依赖项数组”中useEffect 。 (解决问题的合适工具通常是useReduceruseRef 。我们在上面已经讨论过,很快我们将准备用于选择这种方法的材料)。

如果我们应用程序中的大多数代码都基于功能组件,这意味着我们需要更多地了解代码优化 ,以及随着时间的推移哪些值可能会改变

: « , , , , , ».

. , React , . , « », . , React .

, , .


React —

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


All Articles