与React类组件相比,React函数式组件究竟有何不同?

一般的回答都是:

  1. 类组件比函数式组件多了更多的特性,比如 state,那如果有 Hooks 之后呢?
  2. 函数组件性能比类组件好,但是在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
    1. 性能主要取决于代码的作用,而不是选择函数式还是类组件。尽管优化策略有差别,但性能差异可以忽略不计。
    2. 参考官网:(https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render)
    3. 参考作者github:(https://github.com/ryardley/hooks-perf-issues/pull/2)

而下面会重点讲述:React的函数式组件和类组件之间根本的区别: 在心智模型上。

简单的案例

函数式组件以来,它一直存在,但是经常被忽略:函数式组件捕获了渲染所用的值。(Function components capture the rendered values.)

思考这个组件:

function ProfilePage(props) {
const showMessage = () => alert('你好 ' + props.user); const handleClick = () => setTimeout(showMessage, 3000); return <button onClick={handleClick}>Follow</button>
}

上述组件:如果 props.userDan,它会在三秒后显示 你好 Dan

如果是类组件我们怎么写?一个简单的重构可能就象这样:

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 应用程序中的一个常见错误来说明其中的不同。

我们添加一个父组件,用一个下拉框来更改传递给子组件(ProfilePage),的 props.user,实例地址:(https://codesandbox.io/s/pjqnl16lm7) 。

按步骤完成以下操作:

  1. 点击 其中某一个 Follow 按钮。
  2. 在3秒内 切换 选中的账号。
  3. 查看 弹出的文本。

这时会得到一个奇怪的结果:

  • 当使用 函数式组件 实现的 ProfilePage, 当前账号是 Dan 时点击 Follow 按钮,然后立马切换当前账号到 Sophie,弹出的文本将依旧是 'Followed Dan'
  • 当使用 类组件 实现的 ProfilePage, 弹出的文本将是 'Followed Sophie'

在这个例子中,函数组件是正确的。 如果我关注一个人,然后导航到另一个人的账号,我的组件不应该混淆我关注了谁。 ,而类组件的实现很明显是错误的。

案例解析

所以为什么我们的例子中类组件会有这样的表现? 让我们仔细看看类组件中的 showMessage 方法:

  showMessage = () => {
alert('Followed ' + this.props.user);
};

这个类方法从 this.props.user 中读取数据。

  1. 在 React 中 Props 是 不可变(immutable)的,所以他们永远不会改变。
  2. this 是而且永远是 可变(mutable)的。**

这也是类组件 this 存在的意义:能在渲染方法以及生命周期方法中得到最新的实例。

所以如果在请求已经发出的情况下我们的组件进行了重新渲染, this.props将会改变。 showMessage方法从一个"过于新"的 props中得到了 user

从 this 中读取数据的这种行为,调用一个回调函数读取 this.props 的 timeout 会让 showMessage 回调并没有与任何一个特定的渲染"绑定"在一起,所以它"失去"了正确的 props。。

如何用类组件解决上述BUG?(假设函数式组件不存在)

我们想要以某种方式"修复"拥有正确 props 的渲染与读取这些 props 的 showMessage回调之间的联系。在某个地方 props被弄丢了。

方法一:在调用事件之前读取 this.props,然后显式地传递到timeout回调函数中:
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}>Followbutton>;
}
}

然而,这种方法使得代码明显变得更加冗长。如果我们需要的不止是一个props 该怎么办? 如果我们还需要访问state 又该怎么办? 如果 showMessage 调用了另一个方法,然后那个方法中读取了 this.props.something 或者 this.state.something ,我们又将遇到同样的问题。然后我们不得不将 this.propsthis.state以函数参数的形式在被 showMessage调用的每个方法中一路传递下去。

这样的做法破坏了类提供的工程学。同时这也很难让人去记住传递的变量或者强制执行,这也是为什么人们总是在解决bugs。

这个问题可以在任何一个将数据放入类似 this 这样的可变对象中的UI库中重现它(不仅只存在 React 中)

方法二:如果我们能利用JavaScript闭包的话问题将迎刃而解。*

如果你在一次特定的渲染中捕获那一次渲染所用的props或者state,你会发现他们总是会保持一致,就如同你的预期那样:

class ProfilePage extends React.Component {
render() {
const props = this.props; const showMessage = () => {
alert('Followed ' + props.user);
}; const handleClick = () => {
setTimeout(showMessage, 3000);
}; return <button onClick={handleClick}>Follow</button>;
}
}

你在渲染的时候就已经"捕获"了props:。这样,在它内部的任何代码(包括 showMessage)都保证可以得到这一次特定渲染所使用的props。

Hooks 的由来

但是:如果你在 render方法中定义各种函数,而不是使用class的方法,那么使用类的意义在哪里?

事实上,我们可以通过删除类的"包裹"来简化代码:

function ProfilePage(props) {
const showMessage = () => {
alert('Followed ' + props.user);
}; const handleClick = () => {
setTimeout(showMessage, 3000);
}; return (
<button onClick={handleClick}>Follow</button>
);
}

就像上面这样, props仍旧被捕获了 —— React将它们作为参数传递。 不同于 thisprops 对象本身永远不会被React改变。

当父组件使用不同的props来渲染 ProfilePage时,React会再次调用 ProfilePage函数。但是我们点击的事件处理函数,"属于"具有自己的 user值的上一次渲染,并且 showMessage回调函数也能读取到这个值。它们都保持完好无损。

这就是为什么,在上面那个的函数式版本中,点击关注账号1,然后改变选择为账号2,仍旧会弹出 'Followed 账号1'

函数式组件捕获了渲染所使用的值。

使用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>
</>;
}

如果我发送一条特定的消息,组件不应该对实际发送的是哪条消息感到困惑。这个函数组件的 message变量捕获了"属于"返回了被浏览器调用的单击处理函数的那一次渲染。所以当我点击"发送"时 message被设置为那一刻在input中输入的内容。

读取最新的状态

因此我们知道,在默认情况下React中的函数会捕获props和state。 但是如果我们想要读取并不属于这一次特定渲染的,最新的props和state呢?如果我们想要["从未来读取他们"]呢?

在类中,你通过读取 this.props或者 this.state来实现,因为 this本身时可变的。React改变了它。在函数式组件中,你也可以拥有一个在所有的组件渲染帧中共享的可变变量。它被成为"ref":

function MyComponent() {
const ref = useRef(null); }

但是,你必须自己管理它。

一个ref与一个实例字段扮演同样的角色。这是进入可变的命令式的世界的后门。你可能熟悉'DOM refs',但是ref在概念上更为广泛通用。它只是一个你可以放东西进去的盒子。

甚至在视觉上, 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,我们将得到在我们按下发送按钮那一刻的信息。但是当我们读取 latestMessage.current,我们将得到最新的值 —— 即使我们在按下发送按钮后继续输入。

ref是一种"选择退出"渲染一致性的方法,在某些情况下会十分方便。

通常情况下,你应该避免在渲染期间读取或者设置refs,因为它们是可变得。我们希望保持渲染的可预测性。 然而,如果我们想要特定props或者state的最新值,那么手动更新ref会有些烦人。我们可以通过使用一个effect来自动化实现它:

function MessageThread() {
const [message, setMessage] = useState(''); const latestMessage = useRef('');
useEffect(() => {
latestMessage.current = message;
});
const showMessage = () => {
alert('You said: ' + latestMessage.current);
};

我们在一个effect 内部执行赋值操作以便让ref的值只会在DOM被更新后才会改变。这确保了我们的变量突变不会破坏依赖于可中断渲染的时间切片和 Suspense 等特性。

通常来说使用这样的ref并不是非常地必要。 捕获props和state通常是更好的默认值。 然而,在处理类似于intervals和订阅这样的命令式API时,ref会十分便利。你可以像这样跟踪 任何值 —— 一个prop,一个state变量,整个props对象,或者甚至一个函数。

这种模式对于优化来说也很方便 —— 例如当 useCallback本身经常改变时。然而,使用一个reducer 通常是一个更好的解决方式

闭包帮我们解决了很难注意到的细微问题。同样,它们也使得在并发模式下能更轻松地编写能够正确运行的代码。这是可行的,因为组件内部的逻辑在渲染它时捕获并包含了正确的props和state。

函数捕获了他们的props和state —— 因此它们的标识也同样重要。这不是一个bug,而是一个函数式组件的特性。例如,对于 useEffect或者 useCallback来说,函数不应该被排除在"依赖数组"之外。(正确的解决方案通常是使用上面说过的 useReducer或者 useRef

当我们用函数来编写大部分的React代码时,我们需要调整关于优化代码什么变量会随着时间改变的认知与直觉。

到目前为止,我发现的有关于hooks的最好的心里规则是"写代码时要认为任何值都可以随时更改"。

React函数总是捕获他们的值 —— 现在我们也知道这是为什么了。

文章参考:React作者 Dan Abramov 的github

最后

  1. 译者写了一个 React + Hooks 的 UI 库,方便大家学习和使用, (https://github.com/zhongmeizhi/z-ui)
  2. 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。

React Hooks 实现和由来以及解决的问题的更多相关文章

  1. 初探React Hooks & SSR改造

    Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...

  2. 理解 React Hooks

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由志航发表于云+社区专栏 TL;DR 一句话总结 React Hooks 就是在 react 函数组件中,也可以使用类组件(classe ...

  3. 关于React Hooks,你不得不知的事

    React Hooks是React 16.8发布以来最吸引人的特性之一.在开始介绍React Hooks之前,让咱们先来理解一下什么是hooks.wikipedia是这样给hook下定义的: In c ...

  4. react新特性 react hooks

    本文介绍的是react新特性react hooks,本文面向的是有一定react开发经验的小伙伴,如果你对react还不是很熟悉的话我建议你先学习react并多多联系. 首先我们都知道react有3种 ...

  5. 30分钟精通React今年最劲爆的新特性——React Hooks

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周 ...

  6. React Hooks 深入系列 —— 设计模式

    本文是 React Hooks 深入系列的后续.此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子 ...

  7. React Hooks 你不来了解下?

    前言 最近在看 React 的新语法-- React Hooks,只能一句话概括:React 语法真的是越来越强大,越写代码越少. 强烈推荐还没看 React Hooks 的同学去学习下,这会让你写r ...

  8. React Hooks用法大全

    前言 在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖 ...

  9. React Hooks究竟是什么呢?

    摘要: React Hooks原理解析. 原文:快速了解 React Hooks 原理 译者:前端小智 我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件 ...

随机推荐

  1. java 集合之HashMap的三种遍历

    HashMap 是一个散列表,它存储的内容是键值对(key-value)映射. 这周我们只需记住三种遍历方法 1.通过keySet()获取键,再利用hashmap里面的.get(key)方法通过键获取 ...

  2. Django入门2--Django的应用和开发第一个Template

    Django创建应用的命令: 应用的目录: 开发第一个Template:

  3. Codeforces Round #183 (Div. 2)

    A. Pythagorean Theorem II 暴力,\(O(n^2)\). B. Calendar 每个日期计算到0年1月1日的天数,相当于转化成前缀和形式. 闰年数计算\[\lfloor\fr ...

  4. H3C 路由表查找规则(1)

  5. dotnet core 使用 CoreRT 将程序编译为 Native 程序

    现在微软有一个开源项目 CoreRT 能通过将托管的 .NET Core 编译为单个无依赖的 Native 程序 这个项目现在还没发布,但是能尝试使用,可以带来很多的性能提升 使用 CoreRT 发布 ...

  6. CSS多余文本省略号显示

    CSS多余文本省略号显示 本次案例代码是在 elementui 当中的 table 组件中实际需求 当然使用的是纯 CSS3 代码,所以原生支持度高,兼容性高,所以可多场景应用 对于过长文本进行单行省 ...

  7. codemirror 行高 字体 行间距 设置

    用的是 react-codemirror2 样式文件: ... .code-mirror{ font-size : 13px; line-height : 150%; } ... 引入样式文件: .. ...

  8. es6笔记 day1---let和const的应用

    ES6 -> ECMA标准 ES7  ES8 最早是由ECMA-262版本实现的 ---------------------------------------- ES6 也称为ES2015,2 ...

  9. Java虚拟机-类文件结构

    目录 类文件结构 Class类文件的结构 魔数与Class文件的版本 常量池 访问标志 类索引.父类索引和接口索引集合 字段表集合 方法表集合 属性表集合 完整结构描述 实例 源码 Class文件 分 ...

  10. 北京师范大学第十七届程序设计竞赛决赛 G

    传送门:https://ac.nowcoder.com/acm/contest/895/G 题意: \[ 操作 1:L,R,X,Y,对所有L≤i≤R赋值 \\ Ai=min(Ai,(i−L)×Y+X) ...