乐趣区

【译】函数组件和类组件有什么不同?

原文链接:https://overreacted.io/how-ar…

在很长一段时间内,标准答案是 class components 提供更多的特性(像 state)。但随着 Hooks 的出现,答案就不再是这样子了。
或许你听说过他们中的一个性能可能更好,哪一个?因为各种的判断标准获取都存在缺陷,所以我们需要小心仔细的得出结论。性能的好坏主要取决于什么?它主要取决于你的代码在做什么,而不是你使用的是 function 还是 class。在我们的观察中,尽管优化的策略可能会有些许的不同,但性能的差异几乎可以忽略不及。
无论是哪种情况,我们都不建议你重写现有的组件,除非你有一些其他的原因或者是想成为 Hooks 的早期的采用者。Hooks 仍然是一个新特性(就像 2014 年的 React 一样),一些最佳实践还没有被写入到教程中。
那我们该怎么办?function components 和 class components 之间有什么本质的区别吗?显然,在构思模型中是不同的。在这篇文章中,我们将看到他们之间最大的区别,自从 2015 年推出 function componetns 以来,它就一直存在着,但是却经常被忽视:
function components 捕获渲染值 (capture value)
注意:本文并不是对函数或者类的值做判断。我只描述了 React 中这两个编程模型之间的区别。有关更广泛地采用函数的问题,请参阅 hooks 常见问题解答。
思考下面这样一个组件:
function ProfilePage(props) {
const showMessage = () => {
alert(‘Followed ‘ + props.user);
};

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

return (
<button onClick={handleClick}>Follow</button>
);
}
这个组件通过 setTimeout 模拟网络请求,在点击按钮 3 秒后弹出 props.user 的值,如果 props.user 的值是 Dan 的话,他将在点击后 3 秒弹出“Followed Dan”。(注意,使用箭头函数还是函数声明的形式并不重要,handleClick 函数的工作方式完全相同)
如果改写成 class 形式可能长下面这个样子:
class ProfilePage extends React.Component {
showMessage = () => {
alert(‘Followed ‘ + this.props.user);
};

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

render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}
通常认为这两段代码是等效的。但是大家经常在这两种形式之间来回切换代码而不去关注他们的含义。
然而其实这两段代码是有细微的差别的,就我个人而言,我花费了一段时间才看出来。
如果你自己想弄清楚的话,这里有一个在线 demo。本文的剩余部分解释了有什么不同和它的重要性。

再继续之前,我要强调,我所描述的差异本身和 React Hooks 无关,上面的例子甚至都没有使用 Hooks。
这全部都是 React 中,function 和 class 的差别。如果你想要在 React 应用中去频繁的使用 function components,那么你应该去了结它。

我们将用一个在 React 应用程序中常见的错误来说明这一区别。
打开这个示例,有一个主页 select 和两个主页,且每一个包含一个 Follow 按钮。
尝试按照下面的顺序操作:

点击一个 Follow 按钮
改变 select 选项然后等待 3 秒
查看 alert 的文字

你会发现一个问题:

在 function components 中,在 Dan 的主页点击 follow 然后切换到 Sophie,alert 仍然会展示“Followed Dan”。
在 class components 中,alert 的却是“Followed Sophie”。

在这个例子中,第一个行为是正确的。如果我 Follow A,然后导航 B 的主页,我的组件不应该 Follow 到 B。这个 class 显然有缺陷。

所以为什么我们 class 的例子展示出这样的结果呢?
让我们仔细研究一下 class 中的 showMessage 方法:
showMessage = () => {
alert(‘Followed ‘ + this.props.user);
};
这个方法从 this.props.user 取值,在 React 中,props 应该是不可变的,但是 this 却是可变的。
实际上,在 React 内部会随着时间的推移改变 this,以便可以在 render 和生命周期中取到最新的版本。
所以如果我们的组件在请求过程中 re-render,this.props 将会改变,showMessage 方法将会从“最新”的 props 中取到 user 的值。
这就暴露了一个关于 UI 的有趣问题。如果说 UI 是一个关于当前应用 state 的函数,那么事件处理函数就是 render 的一部分,就像是可视化输出一样。我们的事件处理函数“属于“某一特定 state 和 props 的 render。
但是在包含超时操作的回调函数内读取 this.props 会破坏这个关联。showMessage 没有“绑定”到任何一个特定的 render,因此它“丢失”了正确的 props。

我们说 function components 不会存在这个问题。那么我们该怎么去解决呢?
我们需要去用某种方式“修复”正确的 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>;
}
}
这是可行的。然而,这种方法使代码更加冗长,并且随着时间的推移更容易出错。如果我们需要的不仅仅是一个单一的 props 怎么办?如果 ShowMessage 调用另一个方法,而该方法读取 this.props.something 或 this.state.something,我们将再次遇到完全相同的问题。所以我们必须通过在 ShowMessage 调用的每个方法,将 this.props 和 this.state 作为参数传递。
这样做我们通常会破坏一个 class,并且会导致很多 bug 出现。
同样,在 handleClick 中用 alert 展示也不能暴露出更深的问题。如果我们想要去结构化我们的代码,将代码拆分出不同的方法,并且在读取 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>;
}
}
不,这并不能解决任何问题。记住,我们的问题是拿到 this.props 太晚了,而不是我们使用何种语法。但是,如果我们完全依赖于 js 的闭包,问题就会得到解决。
闭包通常是被避免的,因为它很难考虑一个随时间变化的值。但是在 React 中,props 和 state 应该是不可变的。
这意味着,如果去掉某个特定 render 中的 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>;
}
}
已经在 render 时“捕获”到了 props。

这样,它里面的任何代码(包括 showMessage)都可以保证取到某个特定 render 中的 props 了。
然后我们可以添加很多的 helper 函数,他们都可以捕获到 props 和 state。闭包救了我们。

上面的例子是正确的,但看起来很奇怪。如果只是在 render 中定义函数而不是使用类方法,那么我们使用一个 class 又有什么意义呢?
实际上我们可以通过移除 class 来简化代码:
function ProfilePage(props) {
const showMessage = () => {
alert(‘Followed ‘ + props.user);
};

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

return (
<button onClick={handleClick}>Follow</button>
);
}
就像上面所说的,props 仍然可以被捕获到,React 将它作为一个参数传递。不同的是,props 对象本身不会因 React 而发生变化了。
在下面中就更明显了:
function ProfilePage({user}) {
const showMessage = () => {
alert(‘Followed ‘ + user);
};

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

return (
<button onClick={handleClick}>Follow</button>
);
}
当父组件根据不同的 props 渲染时,React 将会再次调用 function,但是我们点击的事件处理函数是上一个包含 user 值的 render,并且 showMessage 函数已经拿到了 user 这个值
这就是为什么在这个版本的 function demo 中在 Sophie 主页点击 Follow,然后改变 select,将会 alert“Followed Sophie”。

现在我们知道了在 React 中 function 和 class 的最大不同。
function components 捕获渲染值(capture value)
对于钩子,同样的原理也适用于 state。考虑这个例子:
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>
</>
);
}
这里是在线 demo
这个例子说明了相同点:在点击 send 按钮后,再次修改输入框的值,3 秒后的输出依然是点击前输入框的值。这说明 function Hooks 同样具有 capture value 的特性。

所以我们知道了在 React 中 function 默认情况下会捕获 props 和 state(capture value)。但是如果我们想要去避免这个 capture value 呢?
在 class 中,我们可以通过使用 this.props 和 this.state,因为 this 本事是可变的,React 改变了它,在 function components 中,还有一个被所有组件所共享的可变值,被叫做 ref:
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// …
}
但是,你必须自己管理它。
ref 和实例字段有着相同的作用,你也许更为熟悉“dom refs”,但是这个概念更为普遍,它仅仅是一个“放置一些东西的通用容器”。
尽管看起来它像是某一些东西的镜像,但实际上他们表示着相同的概念。
React 默认不会为 function components 创建保存最新 props 和 state 的 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;
};
这时我们发现,在点击 send 按钮后继续输入,3 秒后 alert 的是点击按钮后输入的值而不是点击按钮钱输入的值。
通常,应该避免在渲染期间读取或设置 refs,因为它们是可变的。我们希望保持渲染的可预测性。但是,如果我们想要获取特定 props 或 state 的最新值,手动更新 ref 可能会很烦人。我们可以通过使用一种效果来实现自动化(useEffect 在每次 render 都会执行):
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
我们在 effect 中进行赋值,以便 ref 的值只在 DOM 更新后才更改。
像这样使用 ref 不是经常需要的。通常 capture props 或 state 才是默认更好的选择。但是,在处理诸如间隔和订阅之类的命令式 API 时,它非常方便。记住,您可以跟踪任何这样的值:一个 prop、一个 state 变量、整个 props 对象,甚至一个函数。

在本文中,我们研究了 class 中常见的中断模式,以及闭包如何帮助我们修复它。但是,你可能已经注意到,当你试图通过指定依赖数组来优化 Hooks 时,可能会遇到带有过时闭包的错误。这是否意味着闭包是问题所在?我不这么认为。
正如我们上面所看到的,闭包实际上帮助我们解决了难以注意到的细微问题。类似地,它们使在并发模式下正确地编写代码变得更加容易。
到目前为止,我所看到的所有情况下,“过时的闭包”问题都是由于错误地假设“函数不更改”或“props 总是相同”而发生的。事实并非如此,我希望这篇文章能够帮助澄清。
function components 没有 props 和 state,因此它们的也同样重要。这不是 bug,而是 function components 的一个特性。例如,函数不应该从 useEffect 或 useCallback 的“依赖项数组”中被排除。(正确的解决方案通常是上面的 useReducer 或 useRef 解决方案。)
当我们用函数编写大多数 React 代码时,我们需要调整优化代码的直觉,以及什么值会随着时间而改变。
正如 Fredrik 所说:
对于 Hooks,我迄今为止发现的最好的规则是“代码就像任何值在任何时候都可以改变”。
React 的 function 总是捕捉它们的值(capture value)—— 现在我们知道为什么了。

退出移动版