原文连接:overreacted.io/how-are-fun…javascript
在很长一段时间内,标准答案是class components
提供更多的特性(像state)。但随着Hooks的出现,答案就再也不是这样子了。java
或许你据说过他们中的一个性能可能更好,哪个?由于各类的判断标准获取都存在缺陷,因此咱们须要当心仔细的得出结论。性能的好坏主要取决于什么?它主要取决于你的代码在作什么,而不是你使用的是function仍是class。在咱们的观察中,尽管优化的策略可能会有些许的不一样,但性能的差别几乎能够忽略不及。react
不管是哪一种状况,咱们都不建议你重写现有的组件,除非你有一些其余的缘由或者是想成为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函数的工做方式彻底相同)dom
若是改写成class形式可能长下面这个样子:ide
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按钮。
尝试按照下面的顺序操做:
你会发现一个问题:
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)—— 如今咱们知道为何了。