函数组件与类有何不同?
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
-分别呈现“关注”按钮。
使用两个按钮尝试以下操作顺序:
- 单击“跟随”按钮之一。
- 在3秒钟之前更改选定的配置文件。
- 阅读警报文本。
您会发现一个独特的区别:
- 使用上述
ProfilePage
功能,在Dan的个人资料上单击“关注”,然后导航至Sophie's仍会提示'Followed Dan'
。 - 使用上述
ProfilePage
类,它会发出警报'Followed Sophie'
:
在此示例中,第一个行为是正确的行为。如果我关注某人,然后导航至其他人的个人资料,则我的组件不会对我关注的人感到困惑。此类的实现显然是错误的。
(不过,您应该完全跟随苏菲。)
那么为什么我们的类示例会这样表现呢?
让我们仔细看一下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'
:
此行为是正确的。(尽管您可能也想关注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代码时,我们需要调整关于优化代码的直觉,以及随时间变化的值。
到目前为止,用钩子找到的最好的心理规则是“代码好像随时可以更改任何值”。
函数也不例外。这在React学习材料中成为常识将需要一些时间。它需要从班级的心态进行一些调整。但我希望本文能帮助您以崭新的眼光看它。
React函数始终捕获其值-现在我们知道了原因。