原文: overreacted.io/how-are-fun…javascript
在 React.js
开发中,函数组件(function component) 和 类组件(class component) 有什么差别呢?html
在之前,一般认为区别是,类组件提供了更多的特性(好比state
)。随着 React Hooks 的到来,这个说法也不成立了(经过hooks,函数组件也能够有state
和类生命周期回调了)。java
或许你也据说过,这两类组件中,有一类的性能更好。哪一类呢?不少这方面的性能测试,都是 有缺陷的 ,所以要从这些测试中 得出结论 ,不得不谨慎一点。性能主要取决于你代码要实现的功能(以及你的具体实现逻辑),和使用函数组件仍是类组件,没什么关系。咱们观察发现,尽管函数组件和类组件的性能优化策略 有些不一样 ,可是他们性能上的差别是很微小的。react
无论是上述哪一个缘由,咱们都 不建议 你使用函数组件重写已有的类组件,除非你有别的缘由,或者你喜欢当第一个吃螃蟹的人。React Hooks
还很新(就像2014年的React同样),目前尚未使用hooks相关的最佳实践。git
除了上面这些,还有什么别的差别么?在函数组件和类组件之间,真的存在根本性的不一样么?"Of course, there are — in the mental model" (这个实在不知道咋表达,贴下做者原文吧😅) 在这篇文章里,咱们将一块儿看下,这两类组件最大的不一样。这个不一样点,在2015年函数组件函数组件 被引入React 时就存在了,可是被大多数人忽略了。github
函数组件会捕获render内部的状态编程
让咱们一步步来看下,这表明什么意思。数组
**注意,本文并非对函数组件和类组件进行价值判断。我只是展现下React生态里,这两种组件编程模型的不一样点。要学习如何更好的采用函数组件,推荐官方文档 Hooks FAQ ** 。性能优化
假设咱们有以下的函数组件:网络
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
复制代码
组件会渲染一个按钮,当点击按钮的时候,模拟了一个异步请求,而且在请求的回调函数里,显示一个弹窗。好比,若是 props.user
的值是 Dan
,点击按钮3秒以后,咱们将看到 Followed Dan
这个提示弹窗。很是简单。
(注意,上面代码里,使用箭头函数仍是普通的函数,没有什么区别。由于没有 this
问题。把箭头函数换成普通函数 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平常开发中,常常遇到的一个bug,来展现这个差别 。
接下来,咱们就来复现下这个bug。打开 这个在线例子 ,页面上有一个名字下拉框,下面是两个关注组件,一个是前文的函数组件,另外一个是类组件。
对每个关注按钮,分别进行以下操做:
你应该注意到了两次alert弹窗的差异:
Dan
,点击关注按钮,迅速将下拉框切换到Sophie
,3秒以后,alert弹窗内容仍然是 Followed Dan
Followed Sophie
在这个例子里,使用函数组件的实现是正确的,类组件的实现明显有bug。若是我先关注了一我的,而后切换到了另外一我的的页面,关注按钮不该该混淆我实际关注的是哪个 。
(PS,我也推荐你真的关注下 Sophie)
那么,为何咱们的类组件,会存在问题呢?
让咱们再仔细看看类组件的 showMessage
实现:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
复制代码
这个方法会读取 this.props.user
。在React生态里,props
是不可变数据,永远不会改变。可是,this
却始终是可变的 。
确实,this
存在的意义,就是可变的。react在执行过程当中,会修改this
上的数据,保证你可以在 render
和其余的生命周期方法里,读取到最新的数据(props, state)。
所以,若是在网络请求处理过程当中,咱们的组件从新渲染,this.props
改变了。在这以后,showMessage
方法会读取到改变以后的 this.props
。
这揭示了用户界面渲染的一个有趣的事实。若是咱们认为,用户界面(UI)是对当前应用状态的一个可视化表达(UI=render(state)
),那么事件处理函数,一样属于render结果的一部分,正如用户界面同样 。咱们的事件处理函数,是属于事件触发时的render
,以及那次render相关联的 props
和state
。
然而,咱们在按钮点击事件处理函数里,使用定时器(setTimeout)延迟调用 showMessage
,打破了showMessage
和this.props
的关联。showMessage
回调再也不和任何的render绑定,一样丢失了原本关联的props。从 this
上读取数据,切断了这种关联。
假如函数组件不存在,那咱们怎么来解决这个问题呢?
咱们须要经过某种方式,修复showMessage
和它所属的 render
以及对应props的关联。
一种方式,咱们能够在按钮点击处理函数中,读取当前的 props,而后显式的传给 showMessage
,就像下面这样:
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
方法要读取更多的props呢?若是showMessage
还要访问state呢?若是 showMessage
调用了其余的方法,而那个方法读取了别的状态,好比this.props.something
或 this.state.something
,咱们会再次面临一样的问题。 咱们可能须要在 showMessage
里显式的传递 this.props
this.state
给其余调用到的方法。
这样作,可能会破坏类组件带给咱们的好处。这也很难判断,何时须要传递,何时不须要,进一步增长了引入bug的风险。
一样,简单地把全部代码都放在 onClick
处理函数里,会带给咱们其余的问题。为了代码可读性、可维护性等缘由,咱们一般会把大的函数拆分为一些独立的小的函数。这个问题不只仅是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和state是 不可变的(严格来讲,咱们强烈推荐将props和state做为不可变数据)。props和state的不可变特性,完美解决了使用闭包带来的问题。
这意味着,若是在 render
方法里,经过闭包来访问props和state,咱们就能确保,在showMessage
执行时,访问到的props和state就是render执行时的那份数据:
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 。
经过这种方式,render方法里的任何代码,都能访问到render执行时的props,而不是后面被修改过的值。react不会再偷偷挪动咱们的奶酪了。
像上面这样,咱们能够在render方法里,根据须要添加任何帮助函数,这些函数都可以正确的访问到render执行时的props。闭包,这一切的救世主。
上面的代码,功能上没问题,可是看起来有点怪。若是组件逻辑都做为函数定义在render内部,而不是做为类的实例方法,那为何还要用类呢?
确实,咱们剥离掉类的外衣,剩下的就是一个函数组件:
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
复制代码
这个函数组件和上面的类组件同样,内部函数捕获了props,react会把props做为函数参数传进去。和this不一样的是,props是不可变的,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
。可是在这以前,咱们点击关注按钮的事件处理函数,已经捕获了上一次render时的props。
这就是为何,在 这个例子 的函数组件中,没有问题。
能够看到,功能彻底是正确的。(再次PS,建议你也关注下 Sunil)
如今咱们理解了,在函数组件和类组件之间的这个差别:
函数组件会捕获render内部的状态
在有 Hooks
的状况下,函数组件一样会捕获render内部的 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,点击这里)
尽管这是一个很简陋的消息发送组件,它一样展现了和前一个例子相同的问题:若是我点击了发送按钮,这个组件应该发送的是,我点击按钮那一刻输入的信息。
OK,咱们如今知道,函数组件会默认捕获props和state。可是,若是你想读取最新的props、state呢,而不是某一时刻render时捕获的数据? 甚至咱们想在 未来某个时刻读取旧的props、state 呢?
在类组件里,咱们只须要简单的读取 this.props
this.state
就能访问到最新的数据,由于react会修改this
。在函数组件里,咱们一样能够拥有一个可变数据,它能够在每次render里共享同一份数据。这就是hooks里的 useRef
:
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
复制代码
可是,你须要本身维护 ref
对应的值。
函数组件里的 ref
和类组件中的实例属性 扮演了相同的角色 。你或许已经熟悉 DOM refs
,可是hooks里的 ref
更加通用。hooks里的 ref
只是一个容器,你能够往容器里放置任何你想放的东东。
甚至看起来,类组件里的 this.something
也和hooks里的 something.current
类似,他们确实表明了同一个概念。
默认状况下,react不会给函数组件里的props、state创造refs。大多数场景下,你也不须要这样作,这也须要额外的工做来给refs赋值。固然了,你能够手动的实现代码来跟踪最新的state:
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
,咱们会获得输入框里最新的值——甚至咱们在点击发送按钮后,不断的输入新的内容。
你能够对比 这两个demo 来看看其中的不一样。
一般来说,你应该避免在render函数中,读取或修改 refs ,由于 refs 是可变的。咱们但愿能保证render的结果可预测。可是,若是咱们想要获取某个props或者state的最新值,每次都手动更新refs的值显得很枯燥 。这种状况下,咱们可使用 useEffect
这个hook:
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 在这里)
咱们在 useEffect
里去更新 ref 的值,这保证只有在DOM更新以后,ref才会被更新。这确保咱们对 ref 的修改,不会破坏react中的一些新特性,好比 时间切分和中断 ,这些特性都依赖 可被中断的render。
像上面这样使用 ref 不会太常见。大多数状况下,咱们须要捕获props和state 。可是,在处理命令式API的状况下,使用 ref 会很是简便,好比设置定时器,订阅事件等。记住,你可使用 ref 来跟踪任何值——一个prop,一个state,整个props,或者是某个函数。
使用 ref 在某些性能优化的场景下,一样适用。好比在使用 useCallback
时。可是,使用useReducer 在大多数场景下是一个 更好的解决方案 。
在这篇文章里,咱们回顾了类组件中常见的一个问题,以及怎样使用闭包来解决这个问题。可是,你可能已经经历过了,若是你尝试经过指定hooks的依赖项,来优化hooks的性能,那么你极可能会在hooks里,访问到旧的props或state。这是否意味着闭包会带来问题呢?我想不是的。
正如咱们上面看到的,在一些不易察觉的场景下,闭包帮助咱们解决掉这些微妙的问题。不只如此,闭包也让咱们在 并行模式 下更加容易写出没有bug的代码。由于闭包捕获了咱们render函数运行时的props和state,使得并行模式成为可能。
到目前为止,我经历的全部状况下,访问到旧的props、state,一般是因为咱们错误的认为"函数不会变",或者"props始终是同样的"。实时上不是这样的,我但愿在本文里,可以帮助你了解到这一点。
在咱们使用函数来开发大部分react组件时,须要更正咱们对于 代码优化 和 哪些状态会改变 的认知。
正如 Fredrik说的:
在使用react hooks过程当中,我学习到的最重要规则就是,"任何变量,均可以在任什么时候间被改变"
函数一样遵照这条规则。
react函数始终会捕获props、state,最后再强调一下。
译注: 有些地方不明白怎么翻译,有删减,建议阅读原文!