话外:随着 react 新特性 HOOKS 的提出,最新不少人开始讨论 react 里面的函数组件和 class 组件到底有什么不一样?这里是 Dan Abramov 的一篇文章 How Are Function Components Different From Classes, 下面是对这篇文章的翻译,大体意思大体表达出来了,不足的地方,请你们多多指教 :)html
React 函数组件和 React 类有何不一样?react
有一段时间,规范的答案是类提供了更多的功能(如状态)。有了 Hooks,就再也不那么正确了。git
也许你据说其中一个的性能更好。那是哪个呢?许多基于此类的论证过程都存在缺陷,所以我会谨慎地从中得出结论。性能主要取决于代码作了什么,而不是您选择的是函数仍是类。在咱们的观察中,虽然优化策略略有不一样,但性能差别能够忽略不计。github
在任何一种状况下,除非您有其余缘由或者不介意成为第一个使用它的人,不然咱们不建议您使用HOOKS重写现有组件。 Hooks 仍然是新的(就像 2014 年的 React 同样),而且一些“最佳实践”尚未在教程中提到。数组
那咱们该怎么办呢? React 函数和类之间是否有任何根本的区别?固然,还有在心理模型中。在这篇文章中,我将看看它们之间的最大区别。函数组件自 2015 年推出以来,它就一直存在,但却常常被忽视:浏览器
函数组件捕获已渲染的值。网络
让咱们来看看这是什么意思。闭包
注意:这篇文章不是重在评价类或者函数。我只阐述在 react 中的这两个语法模型之间的区别。关于更普遍地采用函数式组件的问题,请参考 Hooks FQA并发
思考一下这个组件:ide
function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
};
const handleClick = () => {
setTimeout(showMessage, 3000);
};
return (
<button onClick={handleClick}>Follow</button>
);
}
复制代码
它显示一个按钮,使用 setTimeout
模拟网络请求,而后显示确认警告弹窗。例如,若是 props.user
为 'Dan'
,那么调用这个函数3s以后将会显示 '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 应用程序中常见的 bug 来讲明它们之间的区别。
使用一个即时 profile 选择器和上面的两个 ProfilePage 实现打开这个示例沙箱 - 每一个都实现了一个 Follow 按钮。
用两个按钮尝试如下操做序列:
你将会注意到他们结果的差别:
ProfilePage
上,点击跟随 Dan 的配置,而后改变配置为 Sophie 的,3s 后的警告任然是 'Followed Dan'
。ProfilePage
上,则将会警告 'Followed Sophie'
在这个例子中,第一个的行为是正确的。若是我跟随了一我的而后导航到另外一我的的配置,个人组件不该该对我到底跟随了谁而感到困惑。这个类的实现显然是错误的。
(尽管你可能想去关注 sophie)
为何咱们使用类的结果是这样的呢?
让咱们仔细观察咱们类中的 showMessage
方法。
class ProfilePage extends React.Component {
showMessage = () => {
alert('Followed ' + this.props.user);
};
复制代码
这个类方法读取 this.props.user
,Props 在 React 中是不可变的因此它永远不会改变,可是 this
老是可变的。
实际上,这就是类中存在this的所有意义。React 自己会随着时间而改变,以便您能够在 render
和生命周期函数中读取新版本。
所以,若是咱们的组件在请求运行时更新。this.props
将会改变。showMessage 方法从“最新”的 props
中读取 user
。
这是揭示了关于用户界面本质的一个有趣的现象。若是咱们说UI在概念上是当前应用程序状态的函数,那么事件处理函数就是渲染输出的一部分,就像可视化输出同样。咱们的事件处理程序“属于”具备特定的 props
和 state
的特定的 render
。
可是当定时器的回调函数读取 this.props
时 打破了这种规则。咱们的 showMessage
回调函数没有绑定到任何特定的 render
,因此它失去了正确的 props
, 从 this
里读取 props 切断了这种关联。
假设说函数组件不存在,咱们该怎么解决这个问题呢?
咱们但愿以某种方式在渲染以后“修复” props 与读取它们的 showMessage
回调之间的链接。由于Props
在传递的过程当中失去了正确的意义。
一个方法是在事件处理函数初始就读取 this.props
,而后精确的将它传递到 setTimeout
的回调函数中。
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
,咱们将再次遇到彻底相同的问题。因此咱们必须将 this.props 和 this.state 做为参数传递给 showMessage
调用的每一个方法。
这么作不只不符合一般咱们对类的认知,同时也极其难以记录并施行,最后代码就会不可避免的出现 bug。
一样,在 handleClick 中 alert
代码并不能解决更大的问题。咱们想要使用一种能够拆分为多个方法的方式来构造代码,同时也要读取被调用时与之对应的参数和状态,这个问题甚至不是 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
。
这样,在这个特定的 render 函数内部的任何代码(包括 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>
);
}
复制代码
就像上面这样,prop
依旧被捕获到了。React 将他们像参数同样进行传递进来,不一样于 this
, 这个 props
对象自己永远不会被React改变。
若是你在函数定义的时候对 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
函数,他本身的 showMessage
回调函数读取的 user
值没有任何改变。
这就是为何在这个例子的函数版本中,点击跟随 sophie's 配置以后改变配置为 sunil 时会显示 ‘Followed Sophie’
,
如今咱们理解了在 React 中函数和类之间最大的区别了:
函数组件获取已经渲染过的值(Function components capture the rendered values.)
使用 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>
</>
);
}
复制代码
(这里是线上例子)
虽然这不是一个很是好的消息应用 UI,但它说明了一样的观点:若是我发送特定消息,组件不该该对实际发送的消息感到困惑。此函数组件的 message
捕获到的是渲染的数据,而后被浏览器调用的单击事件处理函数返回。所以当我点击发送的时候 message
会被设置成我输入的东西。
因此咱们知道 React 里面的函数会默认捕获 props
和 state
。但若是咱们想读取不属于特定 render 的最新 props
和 state
时该怎么办呢?就是若是咱们想从将来读取它们该怎么办?
在类里面,你能够经过读取 this.props
和 this.state
来作到,由于他们自己是可变的。由 React 来改变。在函数组件里,你一样有一个能够被全部组件渲染共享的可变值 ,他被叫作 "ref"。
function MyComponent() {
const ref = useRef(null);
// You can read or write `ref.current`.
// ...
}
复制代码
可是你必须本身去管理它。
ref 与实例字段扮演一样的角色。它是进入可变命令世界的逃脱舱。您可能熟悉 “DOM refs”,但这个的概念更为通用。它只是一个你能够把东西放进去的盒子。
即便在视觉上,this.something
看起来就像是 something.current
同样,它们表达的概念是相同的。
在函数组件里 React 不会默认为最新的 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;
};
复制代码
若是咱们在 showMessage
里读取 message
,当咱们按下 send 按钮的时候咱们会看见这个 message,但当咱们读取 latestMessage.current
时,咱们会获得最新的值,即便是在咱们按下send按钮以后继续输入的值。
你能够经过这两个例子来比较他们的不一样。ref 是一种“选择性退出”渲染一致性的方案,在某些状况下能够很方便。
一般状况下,若是想要保持渲染的可预测性,您应该避免在渲染的时候读取或者设置refs,由于他们是可变的。可是若是咱们想要得到特定的 props
和 state
的最新值,手动更新 ref 可能很烦人。咱们能够经过使用 effect 自动更新它:
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);
};
复制代码
这是一个例子
咱们在 effect 里面作了赋值,因此 ref 的值只会在 DOM 更新以后才会改变,这确保咱们的变化不会破坏依赖于可中断渲染的功能。好比 Time Slicing and Suspense。
一般咱们不须要常用 ref。默认获取 props
和 state
一般更好。可是,在处理 intervals 和 subscriptions 等命令式 API 时它会很方便。请记住,您能够跟踪任何值,好比:prop 和 state 变量、整个 prop 对象、甚至是函数。
此模式的优化也很方便 - 例如,当 useCallback 标识常常更改时。可是,使用 reducer 一般是更好的解决方案。 (将来博客文章的主题!)
在这篇文章中,咱们研究了类中常见的 bug,以及如何使用闭包来帮助咱们修复它。可是,您可能已经注意到,当您试图经过指定依赖项数组来优化 Hooks 时,可能会遇到闭包还将来得及更新致使的 bug。这是否意味着闭包是问题所在呢?我不这么认为。
正如咱们在上面看到的,闭包实际上帮助咱们解决了难以注意到的细微问题。相似地,它们使编写在并发模式下正确工做的代码变得容易得多。这是可能的,由于组件内部的逻辑在渲染它时屏蔽了正确的 props 和 state。
到目前为止,我所见过的全部状况下,“陈旧的闭包”问题都是因为错误地假设“函数不会改变”或 “ props 老是相同的”而发生的。事实并不是如此,我但愿这篇文章有助于澄清这一点。
函数屏蔽了他们更新的 props
和 state
——所以它们的标识也一样重要。这不是一个 bug,而是函数组件的一个特性。例如,对于 useEffect 或 useCallback,函数不该该被排除在“相关数组”以外。(正确的修复一般是 useReducer 或上面的 useRef 解决方案——咱们很快将在文档中解释如何在二者之间进行选择)。
当咱们用函数编写大多数 React 代码时,咱们须要调整咱们关于优化代码的直觉以及哪些值会随时间变化
就像 Fredrik 写的那样:
到目前为止,我所发现的关于 hook 的最好的心理预期是“代码里好像任何值均可以随时更改”。
函数也不例外。这须要一些时间才能成为 React 学习材料的常识。它须要从 class 的思惟方式进行一些调整。但我但愿这篇文章能够帮助你以新的眼光看待它。
React 函数老是捕获它们的值 - 如今咱们知道缘由了。