react作者“Dan”告诉你React函数组件与类组件有什么不同?
原文 how-are-function-components-different-from-classes Dan Abramov 发布于2019年03月03日
一段时间以来,规范的答案是类提供了更多功能(如状态)。但是有了Hooks,这种观点已经不再成立。
也许你听说过其中一个对性能更好。是哪一个?很多此类基准测试都有缺陷,所以我会小心从中得出结论。性能主要取决于代码的执行内容
,而不是你选择了函数还是类。根据我们的观察,性能差异可以忽略不计,尽管优化策略有些不同。
在任何一种情况下,我们都不推荐重写现有组件,除非你有其他理由并且不介意成为早期采用者。Hooks还很新(就像2014年的React一样),一些“最佳实践”还没有进入教程。
那么我们会怎样呢??React函数和类之间真的有本质区别吗?当然,有 —— 在心智模型中。在这篇文章中,我将探讨它们之间最大的不同。自从2015年引入函数组件以来,这种差异一直存在,但经常被忽视:
Function components capture the rendered values.
让我们来解释一下这意味着什么。
注意:这篇文章不是对类或函数的价值判断。我只是描述 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’。很简单。
(注意,在上面的例子中,我使用箭头函数还是函数声明并不重要。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 本身无关。上面的例子甚至没有使用 Hooks!
这都是关于 React 中函数和类之间的区别。如果您打算在 React 应用中更频繁地使用函数,您可能想了解它。
我们将通过一个在React应用中常见的错误来说明这种差异。
打开这个包含当前配置文件选择器的示例沙箱**,以及上述两种 ProfilePage
实现 —— 每个都渲染了一个 Follow 按钮。
尝试对两个按钮执行以下操作序列:
- 单击 其中一个 Follow 按钮
- 在 3 秒内 更改 所选配置文件。
- 阅读alert文本。
你会注意到一个特殊的区别:
- 使用上述
ProfilePage
函数,点击 Dan 的配置文件上的 Follow,然后切换到 Sophie 的配置文件,仍然会警告 ‘Followed Dan’ 。 - 而使用上述
ProfilePage
类,则会警告 ‘Followed Sophie’:
在这个示例中,第一种行为是正确的。如果我关注了一个人,然后切换到另一个人的配置文件,我的组件不应该对我关注的是谁感到困惑。这个类实现显然是有问题的。
(You should totally follow Sophie though.)
(不过你应该关注 Sophie。)
那么为什么我们的类示例会这样呢?
让我们仔细看看我们类中的 showMessage
方法:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
这个类方法从 this.props.user
读取。在 React 中,Props 是不可变的,所以它们永远不会改变。然而,this
是可变的,而且一直都是。
实际上,这是类中 this
的全部用途。React 随时间对其进行变更,以便你可以在 render 和生命周期方法中读取最新的版本。
因此,如果我们的组件在请求进行中重新渲染,this.props
将会改变。showMessage
方法从“最最最新”的 props 中读取用户信息。
这揭示了关于用户界面本质的有趣观察。如果我们说,UI 在概念上是当前应用状态的函数,那么事件处理程序就是渲染结果的一部分——就像视觉输出一样。我们的事件处理程序“属于”特定的渲染,带有特定的 props 和状态。
然而,安排一个超时回调读取 this.props
打破了这种关联。我们的 showMessage
回调不再“绑定”到任何特定的渲染,因此它“失去”了正确的 props。从 this
中读取切断了这种连接。
假设函数组件不存在。我们将如何解决这个问题?
我们希望以某种方式“修复”正确的 props 和读取它们的 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>;
}
}
这种方式可行。然而,这种方法使得代码显著地更加冗长和容易出错。如果我们需要不止一个 prop 呢?如果我们还需要访问状态呢?如果 showMessage
调用了另一个方法,并且那个方法读取了 this.props.something
或 this.state.something
,我们将遇到完全相同的问题。因此,我们将不得不通过从 showMessage
调用的每个方法传递 this.props
和 this.state
作为参数。
这样做违背了使用类的便利性。它也很难记住或执行,这就是为什么人们经常选择忍受错误而不是解决它们的原因。
类似地,将警告代码内联到 handleClick
中并不能解决更大的问题。我们想要以一种方式组织代码,使其既能拆分为更多方法,又能读取与该调用相关的渲染的 props 和状态。这个问题甚至不仅限于 React —— 在任何将数据放入可变对象(如 this)的 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>;
}
}
不,这并没有解决任何问题。记住,问题在于我们读取 this.props
太晚 —— 而不是我们使用的语法!然而,如果我们完全依赖于 JavaScript 的闭包,这个问题就会消失。
闭包通常被避免使用,因为很难思考一个随时间变化的值。但在 React 中,props 和状态是不可变的!(或者至少,强烈建议如此。)这消除了闭包的一个主要隐患。
这意味着,如果你关闭了特定渲染的 props 或状态,你可以始终依赖它们保持完全不变:
class ProfilePage extends React.Component {
render() {
// 捕获 props!
const props = this.props;
// 注意:我们是 *在 render 中*。
// 这些不是类方法。
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
}
你已经“捕获”了渲染时的 props:
这样,其中的任何代码(包括 showMessage
)都保证看到这个特定渲染的 props。React 不再“移动我们的奶酪”。
然后,我们可以在里面添加尽可能多的辅助函数,它们都将使用捕获的 props 和状态。闭包来拯救!
上面的示例是正确的,但看起来很奇怪。如果你在 render 内定义函数而不是使用类方法,那么拥有一个类的意义是什么?
确实,我们可以通过移除围绕它的类“壳”来简化代码:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
就像上面一样,props 仍然被捕获 —— React 将它们作为参数传递。与 this 不同,React 永远不会改变 props 对象本身。
如果你在函数定义中解构 props,这一点会更明显:
function ProfilePage({ user }) {
const showMessage = () => {
alert('Followed ' + user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
当父组件使用不同的 props 渲染 ProfilePage
时,React 将再次调用 ProfilePage
函数。但是我们已经点击的事件处理程序“属于”先前渲染及其自己的user 值和读取它的 showMessage
回调。它们都保持完好无损。
这就是为什么,在这个demo的函数版本中,点击 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,但它说明了同样的观点:如果我发送了一个特定消息,组件不应该对哪条消息被发送感到困惑。这个函数组件的消息捕获了“属于”返回了浏览器调用的点击处理器的渲染的状态。所以消息设置为我点击“发送”时输入框中的内容。
因此,我们知道 React 中的函数默认捕获 props 和状态。但如果我们想读取不属于这个特定渲染的最新 props 或状态怎么办?如果我们想““从未来读取它们””怎么办?
在类中,你可以通过读取 this.props
或 this.state
来做到这一点,因为 this
本身是可变的。React 会改变它。在函数组件中,你也可以有一个可变值,由所有组件渲染共享。它称为“ref”:
function MyComponent() {
const ref = useRef(null);
// 你可以读取或写入 `ref.current`。
// ...
}
然而,你需要自己管理它。
ref 扮演着实例字段相同的角色。它是进入可变命令式世界的逃生舱。你可能熟悉“DOM refs”,但这个概念要广泛得多。它只是一个你可以放入某些东西的盒子。
即使从视觉上看,this.something
看起来像 something.current
的镜像。它们代表相同的概念。
默认情况下,React 在函数组件中不为最新的 props 或状态创建 refs。在许多情况下,你不需要它们,分配它们将是浪费的工作。然而,如果你想,你可以手动跟踪这个值:
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,我们将看到我们按下发送按钮时的消息。但是当我们读取 latestMessage.current
时,我们得到最新的值 —— 即使我们在按下发送按钮后继续输入。
你可以比较两个 演示来自己看看区别。ref 是一种“选择退出”渲染一致性的方式,在某些情况下可能很方便。
通常,你应该避免在渲染期间读取或设置 refs,因为它们是可变的。我们希望保持渲染的可预测性。然而,如果我们想获取特定 prop 或状态的最新值,手动更新 ref 可能会感到烦恼。我们可以通过使用效果(effect)来自动化这一过程:
function MessageThread() {
const [message, setMessage] = useState('');
// 跟踪最新值。
const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};
}
(Here’s a demo.)
我们在效果(effect)内进行赋值,以便只有在 DOM 更新后 ref 的值才会改变。这确保了我们的变更不会破坏依赖可中断渲染的功能,如 Time Slicing 和 Suspense。
使用像这样的 ref 并不是经常必要的。默认情况下捕获 props 或状态通常是更好的选择。然而,当处理诸如间隔和订阅等命令式 API 时,它可能很方便。记住,你可以像这样跟踪任何值——一个 prop,一个状态变量,整个 props 对象,甚至是一个函数。
这种模式对优化也可能很有用——比如当 useCallback 的标识变化太频繁时。然而,使用 reducer 通常是一个更好的解决方案。(一个未来博客文章的主题!)
在这篇文章中,我们探讨了类中常见的破碎模式,以及闭包如何帮助我们修复它。然而,你可能已经注意到,当你尝试通过指定依赖数组来优化 Hooks 时,你可能会遇到过时闭包的问题。这是否意味着闭包是问题所在?我不这么认为。
正如我们上面看到的,闭包实际上帮助我们修复难以察觉的微妙问题。同样,它们使编写在并发模式下正确工作的代码变得更加容易。这是因为组件内的逻辑闭合了与其渲染时的正确 props 和状态。
到目前为止,我所看到的所有“过时闭包”问题都是由于错误地假设“函数不会改变”或“props 总是相同的”。我希望这篇文章有助于澄清这一点。
函数关闭了它们的 props 和状态——因此它们的标识同样重要。这不是 bug,而是函数组件的一个特性。例如,函数不应该从 useEffect 或 useCallback 的“依赖数组”中排除。(正确的修复通常是 useReducer 或上面的 useRef 解决方案——我们将很快记录如何在它们之间选择。)
当我们用函数编写大部分 React 代码时,我们需要调整我们关于优化代码的直觉以及哪些值可能随时间变化。
正如 Fredrik 所说:
我到目前为止找到的最好的心理规则是“假设任何值都可以随时改变”。
函数也不例外。这个观点成为 React 学习材料中的常识需要一些时间。它需要从类思维模式中做出一些调整。但我希望这篇文章帮助你用新的眼光看待它。
React 函数总是捕获它们的值——现在我们知道为什么了。
他们是完全不同的皮卡丘。
原文链接:https://juejin.cn/post/7348760857848184844 作者:吴彦祖火星分祖