0
点赞
收藏
分享

微信扫一扫

函数组件与类有何不同?

React函数组件与React类有何不同?

一段时间以来,规范的答案一直是类提供对更多功能(如状态)的访问。使用​​Hooks​​不再是事实。

也许您已经听说其中之一的性能更好。哪一个?许多这样的基准都是​​有缺陷的,​​​因此我会谨慎地从中​​得出结论​​​。性能主要取决于代码在做什么,而不取决于您选择的是函数还是类。在我们的观察中,性能差异可以忽略不计,但优化策略是有点​​不同​​。

无论哪种情况,除非您有其他原因并且不介意成为早期采用者,否则我们​​都不建议​​您重写现有组件。挂钩仍然很新(就像React在2014年一样),并且一些“最佳实践”还没有进入教程。

那那把我们留在哪里呢?React函数和类之间根本没有根本区别吗?当然,在心理模型中有。在本文中,我将探讨它们之间的最大区别。自从2015年​​引入​​功能组件以来,它就一直存在,但经常被人们忽略:


功能组件捕获呈现的值。


让我们解开这意味着什么。

注意:本文不是对类或函数的价值判断。我只是在React中描述这两种编程模型之间的区别。有关更广泛地采用功能的问题,请参阅​​Hooks FAQ​​。

考虑以下组件:

function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return (
<button onClick={handleClick}>Follow</button>
);
}

它显示一个模拟网络请求的按钮,​​setTimeout​​然后显示一个确认警报。例如,如果​​props.user​​为​​'Dan'​​,它将​​'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 Hooks本身无关。上面的示例甚至都没有使用钩子!

都是关于React中函数和类之间的区别。如果您打算在React应用程序中更频繁地使用函数,那么您可能想了解它。

我们将通过React应用程序中常见的错误来说明差异。

使用当前的配置文件选择器和上面的两个实现打开此​​示例沙箱​​​​ProfilePage​​-分别呈现“关注”按钮。

使用两个按钮尝试以下操作顺序:

  1. 单击“跟随”按钮之一。
  2. 在3秒钟之前更改选定的配置文件。
  3. 阅读警报文本。

您会发现一个独特的区别:

  • 使用上述ProfilePage
    功能,在Dan的个人资料上单击“关注”,然后导航至Sophie's仍会提示'Followed Dan'
  • 使用上述ProfilePage
    类,它会发出警报'Followed Sophie'

函数组件与类有何不同?_解决方案_02

在此示例中,第一个行为是正确的行为。如果我关注某人,然后导航至其他人的个人资料,则我的组件不会对我关注的人感到困惑。此类的实现显然是错误的。

(不过,您应该完全​​跟随苏菲​​。)

那么为什么我们的类示例会这样表现呢?

让我们仔细看一下​​showMessage​​我们类中的方法:

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

此类方法从中读取​​this.props.user​​。道具在React中是不可变的,因此它们永远不会改变。但是,​​this​​ ,并且一直都是可变的。确实,那​​this​​是课堂上的全部目的。随着时间的推移,React自身会对其进行变异,以便您可以在​​render​​和生命周期方法中阅读最新版本。因此,如果在请求进行过程中我们的组件重新呈现,则​​this.props​​它将更改。该​​showMessage​​方法​​user​​从“太新”中读取​​props​​。

这暴露了有关用户界面性质的有趣观察。如果说UI从概念上来说是当前应用程序状态的函数,则事件处理程序是呈现结果的一部分-就像可视输出一样。我们的事件处理程序“属于”具有特定道具和状态的特定渲染。

但是,安排一个超时时间(其回调读取)​​this.props​​会破坏该关联。我们的​​showMessage​​回调未“绑定”到任何特定的渲染,因此它“丢失”了正确的道具。从阅读中​​this​​切断了这种联系。

假设功能组件不存在。我们将如何解决这个问题?

我们想要以某种方式“修复”​​render​​具有正确道具和​​showMessage​​读取道具的回调之间的连接。一路上​​props​​迷路了。一种方法是​​this.props​​在事件期间及早阅读,然后将其显式传递给超时完成处理程序:

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​​调用另一个方法,并且该方法读取​​this.props.something​​或​​this.state.something​​,我们将再次遇到完全相同的问题。因此,我们将必须将​​this.props​​和​​this.state​​作为参数传递给from调用的每个方法​​showMessage​​。

这样做会打败一堂课通常提供的人机工程学。这也很难记住或执行,这就是为什么人们经常选择解决错误的原因。

同样,内联​​alert​​代码​​handleClick​​无法解决更大的问题。我们希望以一种可以将其拆分为更多方法的方式来构造代码,而且还可以读取与该调用相关的渲染相对应的props和state。这个问题甚至不是React独有的-您可以在将数据放入可变对象(如)的任何UI库中重现此问题​​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() {
// Capture the props! const props = this.props;
// Note: we are *inside render*.
// These aren't class methods.
const showMessage = () => {
alert('Followed ' + props.user); };

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return <button onClick={handleClick}>Follow</button>;
}
}

您已经在渲染时“捕获”了道具

这样,​​showMessage​​保证其中的任何代码(包括)都能看到该特定渲染器的道具。React不再“移动我们的奶酪”。

然后,我们可以在内部添加任意数量的帮助程序函数,并且它们都将使用捕获的道具和状态。关闭救援!

​​上面​​​的​​示例​​​是正确的,但看起来很奇怪。如果在内部定义函数​​render​​而不使用类方法,那么拥有一个类有什么意义?

实际上,我们可以通过删除周围的类“ shell”来简化代码:

function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return (
<button onClick={handleClick}>Follow</button>
);
}

就像上面一样,​​props​​仍然被捕获-React将它们作为参数传递。不像​​this​​,​​props​​对象本身不会被React突变。如果您​​props​​在函数定义中进行分解,则会更加明显:

function ProfilePage({ user }) {  const showMessage = () => {
alert('Followed ' + user); };

const handleClick = () => {
setTimeout(showMessage, 3000);
};

return (
<button onClick={handleClick}>Follow</button>
);
}

当父组件​​ProfilePage​​使用不同的道具进行渲染时,React将​​ProfilePage​​再次调用该函数。但是我们已经单击了事件处理程序,该事件处理程序使用其自己的​​user​​值和​​showMessage​​读取该事件的回调单击了“属于”先前的渲染。他们都完好无损。这就是为什么在​​此演示​​​的功能版本中,单击Sophie的配置文件上的“关注”,然后将选择更改为Sunil会提示​​'Followed Sophie'​​:

函数组件与类有何不同?_解决方案_03

此行为是正确的。(尽管您可能也想​​关注Sunil​​!)

现在我们了解了React中函数和类之间的巨大区别:


功能组件捕获呈现的值。


对于Hooks,相同的原理也适用于状态。考虑以下示例:

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>
</>
);
}

(这是​​现场演示​​。)

虽然这不是一个很好的消息应用程序UI,但它说明了同一点:如果我发送特定消息,则组件不应对实际发送的消息感到困惑。该功能组件​​message​​捕获“属于”渲染的状态,该状态返回了浏览器调用的单击处理程序。因此,​​message​​将设置为当我单击“发送”时输入的内容。

因此,我们知道React默认会捕获props和state中的函数。但是,如果我们阅读不属于该特定渲染器的最新道具或状态怎么办?如果我们想​​“从未来阅读它们”​​怎么办?

在课堂上,您可以通过阅读​​this.props​​或​​this.state​​因为​​this​​它本身是可变的来实现。React使它变异。在功能组件中,还可以具有所有组件渲染器共享的可变值。它被称为“参考”:

function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}

但是,您必须自己进行管理。

引用​​与​​​实例字段​​具有相同的作用​​。这是进入可变命令性世界的逃生门。您可能熟悉“ DOM refs”,但是这个概念更为笼统。这只是一个盒子,您可以在其中放一些东西。

即使在视觉上,也​​this.something​​看起来像的镜子​​something.current​​。它们代表相同的概念。

默认情况下,React不会为功能组件中的最新道具或状态创建引用。在许多情况下,您不需要它们,分配它们将浪费大量的工作。但是,您可以根据需要手动跟踪值:

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; };

如果我们读​​message​​入​​showMessage​​,则在按下“发送”按钮时将看到消息。但是,当我们阅读时​​latestMessage.current​​,即使在按下“发送”按钮后继续键入,我们也会获得最新的值。

您可以比较这​​两个​​​ ​​演示​​以自己查看差异。引用是一种“退出”渲染一致性的方法,在某些情况下可以方便使用。

通常,应避免渲染过程中读取或设置ref 因为它们是可变的。我们希望使渲染可预测。但是,如果我们要获取特定道具或状态的最新值,则手动更新ref可能会很烦人。我们可以使用效果使其自动化:

function MessageThread() {
const [message, setMessage] = useState('');

// Keep track of the latest value. const latestMessage = useRef(''); useEffect(() => { latestMessage.current = message; });
const showMessage = () => {
alert('You said: ' + latestMessage.current); };

(这里是一个​​演示​​。)

我们效果进行分配,以便ref值仅在DOM更新后才更改。这确保了我们的变异不会破坏依赖于可中断渲染的“​​时间片”和“暂挂”等​​功能。

不需要经常使用这样的ref。捕获道具或状态通常是更好的默认设置。但是,在处理诸如间隔和订阅之类的​​命令性API​​时可能会很方便。请记住,您可以跟踪任何这样的值-一个prop,一个状态变量,整个props对象甚至一个函数。

这种模式也可以方便地进行优化-例如,当​​useCallback​​身份更改过于频繁时。但是,​​​使用减速器​​​通常是​​更好的解决方案​​。(有关未来博客文章的主题!)

在本文中,我们研究了类中常见的损坏模式,以及闭包如何帮助我们修复该模式。但是,您可能已经注意到,当您尝试通过指定依赖项数组来优化Hooks时,您可能会遇到带有过期闭包的bug。这是否意味着关闭是问题所在?我不这么认为。

正如我们在上面看到的,闭包实际上可以帮助我们解决难以发现的细微问题。同样,它们使编写在​​并发模式下​​正常工作的代码变得容易得多。这是可能的,因为组件内部的逻辑关闭了渲染该组件的正确属性和状态。

到目前为止,在所有情况下,“过时的关闭”问题都是由于错误地假设“功能不变”或“道具始终相同”而发生的。情况并非如此,因为我希望这篇帖子有助于澄清。

功能封闭了它们的道具和状态-因此,它们的身份同样重要。这不是错误,而是功能组件的功能。例如,不应将功能从“依赖项数组”中排除为​​useEffect​​或​​useCallback​​。(正确的解决方案通常是​​useReducer​​上述​​useRef​​解决方案之一,或者是上述解决方案-我们很快会记录如何在它们之间进行选择。)

当我们使用函数编写大多数React代码时,我们需要调整关于​​优化代码​​​的直觉,以及​​随时间变化的值​​。

正如​​弗雷德里克(Fredrik)所说​​:


到目前为止,用钩子找到的最好的心理规则是“代码好像随时可以更改任何值”。


函数也不例外。这在React学习材料中成为常识将需要一些时间。它需要从班级的心态进行一些调整。但我希望本文能帮助您以崭新的眼光看它。

React函数始终捕获其值-现在我们知道了原因。






举报

相关推荐

0 条评论