功能性组建和Classes有什么不同?

37次阅读

共计 6262 个字符,预计需要花费 16 分钟才能阅读完成。

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);
};
demo
结论
在这篇文章中,我们研究了类中常见的破碎模式,以及闭包如何帮助我们修复它。但是,您可能已经注意到,当您尝试通过指定依赖关系数组来优化 Hook 时,您可能会遇到带有过时闭包的错误。是否意味着闭包是问题?我不这么认为。
正如我们上面所看到的,闭包实际上帮助我们解决了很难注意到的细微问题。同样,它们使编写在并发模式下正常工作的代码变得更加容易。这是可以的,因为组件内部的逻辑关闭了正确的 props 和渲染 state。在我到目前为止看到的所有情况中,由于错误地假设“功能不会改变”或“props 总是相同”,所以会出现“陈旧的封闭”问题。事实并非如此,因为我希望这篇文章有助于澄清。

正文完
 0