原文:How Are Function Components Different from Classes?javascript
译者: github.com/joe06102html
React 里的函数组件和类组件有哪些不一样?java
一度,二者的区别在于类组件能提供更多的能力(好比局部的状态)。可是有了Hooks以后,状况却有所不一样了。react
也许以前你据说性能也是二者的差异。可是哪一个性能更好?很差说。我一直很谨慎地对待这类结论,由于不少性能测试都是不全面的。性能主要仍是在于代码的逻辑而非你选择了函数组件或者类组件。据咱们观察,虽然二者的优化策略有所差异,可是总体来讲性能区别是微不足道的。git
不管哪一种状况下,咱们都不推荐用 Hooks 重写你现有的组件。除非你有其余缘由而且不介意作第一个吃螃蟹的人。由于 Hooks 还不是很成熟(就像 2014 年的 React),有一些“最佳实践”尚待发掘。github
因此咱们该怎么办?函数组件和类组件本质上难道没有区别吗?固然有,二者在心智模型上有区别,也就是接下来我要说的二者之间最大的区别。早在 2015 年,函数组件被引入的时候就存在了,只是它经常被忽略。编程
函数组件可以捕获渲染后的值数组
下面让咱们一块儿来理解一下这句话。浏览器
注意: 本文只是描述函数组件和类组件二者在 React 编程模型中的区别,不涉及二者之间的取舍。有关函数组件接受度更高的问题,能够参考Hooks FAQ。网络
假若有以下组件:
function ProfilePage(props) {
const showMessage = () => {
alert("Followed " + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return <button onClick={handleClick}>Follow</button>;
}
复制代码
它有一个按钮,经过 setTimeout 来模拟网络请求,而后显示一个确认弹窗。若是 props.user 是 Dan,3 秒后弹窗就会显示‘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 应用里常见的 Bug 来讲明这个差异。
打开这个示例,里面有一个表示当前用户的下拉框以及上面实现的两个关注组件 -- 每一个都渲染了一个关注按钮。
按照下面的顺序,分别操做两个的按钮:
你会看到一个奇怪的现象:
在这个例子里,表现正确的应该是第一个。若是我点击关注了一我的,而后切换到另外一我的,个人组件应该知道我最后关注了谁。 类组件的表现很明显不正确。
(可是你真的应该关注Sophie。)
因此类组件的表现为何是这样的?
让咱们仔细看下showMessage
这个方法:
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
复制代码
这个方法从this.props.user
中读取用户。在 React 中,props 是不可变的,因此它们不会改变。可是,this
一直都是可变的。
事实上,这就是this
在 class 中的目的。React 经过常常的更新 this 来保证你在 render 和生命周期方法中总能读取到最新版本的数据。
因此,当咱们请求期间,this.props 改变了,showMessage
方法就会从’太新‘的 props 中读取用户信息。
这其实展露了一个关于 UI 本质的现象。若是概念上来讲 UI 是应用当前状态的一种映射,那处理事件其实也是渲染的一部分结果 - 就像视觉上的渲染输出同样。咱们的处理事件其实”属于“某个特定 props 和 state 生成的特定渲染。
可是,延时任务打破了这种关联。咱们的showMessage
回调再也不和特定的渲染绑定,因此就”失去“了其正确的 props。正是从 this 中读取这个行为,切断了二者之间的关联。
假设函数组件不存在,咱们会怎么解决这类问题?
咱们想沿着 props 丢失的地方,以某种方式修复正确的 props 生成的渲染和 showMessage 回调之间的关联。
其中一种方法就是尽早地从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>;
}
}
复制代码
上面这种方法能够奏效。可是,随着时间的推移,这样的代码会变得很啰嗦,而且容易出错。若是须要更多属性怎么办?若是还要访问 state 呢?若是showMessage
又调用了别的函数,而这个函数又从 props 或者 state 读取了某些属性,咱们又会遇到一样的问题。因此咱们仍是得传递 this.props 和 this.state 给每一个在 showMessage 中被调用的函数。
这么作很容易下降效率。并且开发人员很容易忘记,也很难保证必定会按照上面的方法来避免问题,因此常常要去解决这类 bug。
一样的,把alert
代码内联到handleClick
也解决不了这个问题。咱们但愿以更细的粒度来组织代码,同时也但愿能读取和特定渲染对应的 props 和 state 值。这个问题其实非 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 关住,你就能够保证他们对应的都是特定渲染的结果:
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
)就能保证读取到的都是此次渲染的 props。React 也就”动不了咱们的奶酪”了。
而后咱们想加多少函数就能加多少,而且它们都能读取到正确的 props 或者 state。闭包简直是救星!
可是上面的例子看起来有些奇怪。既然都有类了,咱们为何还要在 render 函数里定义函数,而不直接定义类的方法?
事实上,咱们能够经过拿掉“class”的外壳来简化代码:
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的函数式版本中,点击 Follow Sophie,而后切换到 Sunil,却仍然提示Followed Sophie
:
如今,咱们理解了 React 中函数式组件和类组件之间最大的区别就是:
函数式组件能够捕获渲染的值。
在 Hooks 中,这个规则也一样适用于 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)
尽管这不是一个很好的聊天应用界面,可是它也能说明一样的问题:若是我发送了一条消息,应用应该清楚地知道我发送了哪条消息。这个函数组件中的message
捕获了特定渲染的 state,而浏览器所点击的事件处理函数也是渲染结果的一部分。因此message
就是我点击“Send”时输入的值。
如今咱们知道了 React 中的函数默承认以捕获 props 和 state。可是若是咱们想访问不属于此次渲染的最新的 props 或 state 呢?或者是想要稍后再访问它们呢?
在类组件中,你会经过读取this.props
或者this.state
来实现由于 this 自己是可变的。React 会修改它。在函数组件中,你能够拥有一个可变的值,而且它在每次组件渲染的时候都是能够被共享的。这就是ref
:
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
复制代码
可是,你必需要本身管理它。 ref扮演了一个实例字段的角色。它就像是逃往可变的、命令式世界的出口。你可能对”Dom Refs“比较熟悉,可是 ref 的概念更加通用一些。它就是一个你能够往里面装些东西的盒子。
甚至在视觉上,this.somthing
看起来就像是另外一个something.current
。它们有着一样的含义。
默认状况下,React 不会在函数式组件中为最新的 props 或者 state 建立 ref。由于大多数状况下你不须要它,声明它就会有点浪费。可是若是你须要,能够手动跟踪这些值。
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 或 state 的最新值,手动更新 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);
};
复制代码
这是示例
咱们经过在反作用中给 ref 赋值来保证 ref 的值只在每次 DOM 更新以后才会改变。这样能够确保咱们的改变不会影响别的功能例如Time Slicing
和 Suspense
,由于它们依赖于非连续的渲染。
一般咱们不须要这样使用 Ref。大部分状况下,捕获 props 或 state 是更好的方式。可是,当处理一些命令式的 APIs 像interval
和subscriptions
会比较方便。记住你能够像使用 this 同样来追踪任何值 -- 一个属性,一个状态变量,整个 props 对象,甚至是一个函数。
这个模式能够方便地优化代码 -- 例如useCallback
的依赖改变的过于频繁时。可是,使用 reducer 一般是一个更好地选择(又是一个能够用一篇文章来讨论的新特性!)
在这片文章里,咱们了解了类中常见的问题,以及闭包是如何帮咱们解决的。可是,当你想要指定依赖来优化 Hooks 的时候,可能会遇到过时的闭包问题。这是否意味着闭包才是问题所在呢?我不这么认为。
就像咱们上面看到的,闭包帮助咱们解决了一些平时很难注意到的细微的问题。而且,它也能帮咱们方便地写出在并发模式下也能够正常工做的代码。这是由于组件内部逻辑绑定了它渲染时所对应的的 props 和 state。
在我所了解的案例中,“过时的闭包”问题都是由于开发者错误的认为“函数不会改变”或者“props 一直都是相同的”。但事实并不是如此,但愿我这篇文章里已经解释清楚了。
函数组件会和 props 和 state 绑定,因此确认它们的对应性很重要。这不是 bug,而是函数组件的特色。例如,函数也不该该从 useEffect 或者 useCallback 的依赖中被移除。(正确的作法是使用 useReducer 或者 useRef 解决,咱们很快就会出一份关于 2 者如何选择的文档)。
当咱们在咱们的 React 项目中大量使用函数组件时,咱们须要调整对代码优化以及哪些值会随着时间改变的见解。
就像Fredrik 说得:
到目前为止,我发现使用 hooks 最好的心智规则就是:编码的时候当作任何值在任什么时候候都会改变。
函数也不该该排除在这条规则以外。可是把它当作 React 学习中的常识还须要一段时间。由于它要求开发者从类组件的心智模型中作一些调整。可是我但愿这篇文章能帮你从一个全新的视角看待这个问题。
React 函数组件老是能捕获它们的值 -- 如今咱们知道了为何。
由于它们是各类不一样的神奇宝贝。