React函数组件与React类有何不同?
有一段时间,规范的答案是类可以访问更多功能(如状态)。但是自从有了Hook后,这个答案变得不唯一了。
也许你听说其中一个表现更好。哪一个?许多此类基准都存在缺陷,因此我会小心地从中得出结论。性能主要取决于代码的作用,而不是您选择的是函数还是类。在我们的观察中,虽然优化策略有点不同,但性能差异可以忽略不计。
在任何一种情况下,除非您有其他原因并且不介意成为早期采用者,否则我们不建议您重写现有组件。Hooks仍然是新的。
那么功能性函数和类是否又根本的区别?
函数组件捕获rendered的值。
让我们看看这句话是什么意思?
注意:这篇文章不是对类或函数的值判断。我只描述了React中这两种编程模型之间的区别。有关更广泛地采用功能的问题,请参阅Hooks常见问题解答。
思考一下这个组件:
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'。很简单。(注意在上面的例子中我是否使用箭头或函数声明并不重要。函数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本身无关。以上示例甚至不使用Hooks!这就是React中函数和类之间的区别。如果您计划在React应用程序中更频繁地使用函数,则可能需要了解它。
我们将通过React应用程序中常见的错误说明其差异。
打开此示例
使用两个按钮尝试以下操作序列:
- 单击其中一个“关注”按钮。
- 在3秒之前更改所选的配置文件。
- 阅读警报文字。
你会注意到一个特殊的区别:
- 使用上面的ProfilePage函数,单击Follow on Dan的个人资料,然后导航到Sophie's仍然会提醒'Followed Dan'。
- 使用上面的ProfilePage类,它会警告'Followed Sophie':
在此示例中,第一个行为是正确的行为。如果我跟随一个人然后导航到另一个人的个人资料,我的组件不应该让使用的人感到困惑。这个类实现显然是错误的。
那么为什么我们的类示例会以这种方式运行?
让我们仔细看看我们类中的showMessage方法:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
此类方法从this.props.user
读取。Props
在React
中是不可变的,因此它们永远不会改变。然而,这一直是,并且一直是可变的。
React本身会随着时间的推移而变异,以便您可以在渲染和生命周期方法中阅读新版本。
因此,如果我们的组件在请求处于运行状态时render
,则this.props
将会更改。showMessage
方法从“too new”的props
中读取用户。
这暴露了一个关于用户界面性质的有趣观察。如果我们说UI在概念上是当前应用程序状态的函数,则事件处理程序是渲染结果的一部分 - 就像视觉输出一样。我们的事件处理程序“属于”具有特定props
和state
的特定渲染。
假设功能组件不存在。我们如何解决这个问题?
一种方法是在事件期间尽早读取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
,我们将再次遇到完全相同的问题。所以我们必须通过showMessage调用的每个方法将this.props
和this.state
作为参数传递。
同样,在handleClick中内联警报代码并不能解决更大的问题。我们希望以允许将其拆分为更多方法的方式构造代码,同时还要读取与该调用相关的渲染所对应的props
和state
。这个问题甚至不是React独有的 - 您可以在任何将数据放入像这样的可变对象的UI库中重现它。
也许,我们可以绑定构造函数中的方法?
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>;
}
}
不,这不能解决任何问题。请记住,问题是我们从这里读取。支持太晚了 - 不是我们正在使用的语法!但是,如果我们完全依赖JavaScript闭包,问题就会消失。
通常会避免闭包,因为很难想象随着时间的推移可能会发生变异的价值。但在React中,props
和state
是不可改变的!(或者至少,这是一个强烈的推荐。)这消除了一个主要的封闭区域。
这意味着如果你从特定渲染中关闭props
或state
,它们的值保持完全相同:
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>;
}
}
你在渲染时“捕获”了props
:
这样,它内部的任何代码(包括showMessage)都可以保证看到这个特定渲染的道具。React不再“move our cheese”了。
然后我们可以在里面添加任意数量的辅助函数,它们都会使用捕获的props
和state
。
上面的例子是正确的,但看起来很奇怪。如果在render中定义函数而不是使用类方法,那么有一个类是什么意思?
实际上,我们可以通过删除它周围的类“shell”来简化代码:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
就像上面一样,props
仍然被捕获 - React将它们作为参数传递。与此不同,props
对象本身永远不会被React变异。如果你在函数定义中构造props
,那就更明显了:
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
当父组件使用不同的props
呈现ProfilePage时,React将再次调用ProfilePage函数。但是我们已经点击的事件处理程序“属于”具有自己的用户值的前一个渲染和读取它的showMessage回调。他们都完好无损。
现在我们了解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,但它说明了同样的观点:如果我发送特定消息,组件不应该对实际发送的消息感到困惑。此函数组件的消息捕获“属于”渲染器的状态,该渲染器返回浏览器调用的单击处理程序。因此,当我单击“发送”时,消息将设置为输入中的内容。
因此,默认情况下,我们知道React捕获道具和状态中的函数。但是,如果我们想要阅读不属于这个特定渲染的最新道具或州,该怎么办?如果我们想 “read them from the future”怎么办?
在类中,你可以通过阅读this.props或this.state来实现它,因为它本身是可变的。React改变了它。在函数组件中,您还可以具有由所有组件呈现共享的可变值。它被称为“ref”:
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
但是,您必须自己管理它。
ref与实例字段扮演相同的角色。它是进入可变命令世界的逃脱舱。您可能熟悉“DOM refs”,但概念更为通用。它只是一个盒子,你可以把东西放进去。即使在视觉上,这个东西看起来像是某种东西的镜子。它们代表了相同的概念。默认情况下,React不会为函数组件中的最新props
或状态创建引用。在许多情况下,您不需要它们,分配它们将是浪费的工作。但是,如果您愿意,可以手动跟踪值:
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中读取消息,我们会在按下“发送”按钮时看到消息。但是当我们读取latestMessage.current时,我们得到最新的值 - 即使我们在按下发送按钮后继续输入。你可以比较两个演示(https://codesandbox.io/s/93m5... https://codesandbox.io/s/ox200vw8k9),看看差异。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);
};
结论
在这篇文章中,我们研究了类中常见的破碎模式,以及闭包如何帮助我们修复它。但是,您可能已经注意到,当您尝试通过指定依赖关系数组来优化Hook时,您可能会遇到带有过时闭包的错误。是否意味着闭包是问题?我不这么认为。
正如我们上面所看到的,闭包实际上帮助我们解决了很难注意到的细微问题。同样,它们使编写在并发模式下正常工作的代码变得更加容易。这是可以的,因为组件内部的逻辑关闭了正确的props
和渲染state
。
在我到目前为止看到的所有情况中,由于错误地假设“功能不会改变”或“props总是相同”,所以会出现“陈旧的封闭”问题。事实并非如此,因为我希望这篇文章有助于澄清。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。