React Hooks 实现和由来以及解决的问题详解


Posted in Javascript onJanuary 17, 2020

与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.user是 Dan,它会在三秒后显示 你好 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 中读取数据。

在 React 中 Props 是 不可变(immutable)的,所以他们永远不会改变。

而 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.props和 this.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将它们作为参数传递。 不同于 this , props 对象本身永远不会被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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
用方法封装javascript的new操作符(一)
Dec 25 Javascript
jQuery中hide()方法用法实例
Dec 24 Javascript
Javascript中this关键字的一些小知识
Mar 15 Javascript
jQuery中attr()与prop()函数用法实例详解(附用法区别)
Dec 29 Javascript
jQuery插件制作的实例教程
May 16 Javascript
JS中作用域和变量提升(hoisting)的深入理解
Oct 31 Javascript
文件上传插件SWFUpload的使用指南
Nov 29 Javascript
ES6中Proxy代理用法实例浅析
Apr 06 Javascript
设置cookie指定时间失效(实例代码)
May 28 Javascript
Node.js利用js-xlsx处理Excel文件的方法详解
Jul 05 Javascript
vue2实现可复用的轮播图carousel组件详解
Nov 27 Javascript
ajax前台后台跨域请求处理方式
Feb 08 Javascript
详解搭建一个vue-cli的移动端H5开发模板
Jan 17 #Javascript
jQuery实现王者荣耀手风琴效果
Jan 17 #jQuery
JS实现transform实现扇子效果
Jan 17 #Javascript
jQuery 判断元素是否存在然后按需加载内容的实现代码
Jan 16 #jQuery
浅谈VUE中演示v-for为什么要加key
Jan 16 #Javascript
Vue引入Stylus知识点总结
Jan 16 #Javascript
js实现提交前对列表数据的增删改查
Jan 16 #Javascript
You might like
php设计模式 Proxy (代理模式)
2011/06/26 PHP
PHP 伪静态技术原理以及突破原理实现介绍
2013/07/12 PHP
PHP获取QQ达人QQ信息的方法
2015/03/05 PHP
php常用经典函数集锦【数组、字符串、栈、队列、排序等】
2019/08/23 PHP
jquery ajax请求实例深入解析
2012/11/26 Javascript
javascript 实现简单的table排序及table操作练习
2012/12/28 Javascript
javascript Event对象详解及使用示例
2013/11/22 Javascript
js中精确计算加法和减法示例
2014/03/28 Javascript
clipboard.js无需Flash无需依赖任何JS库实现文本复制与剪切
2015/10/10 Javascript
快速学习jQuery插件 Cookie插件使用方法
2015/12/01 Javascript
VUE元素的隐藏和显示(v-show指令)
2017/06/23 Javascript
AngularJS学习笔记之表单验证功能实例详解
2017/07/06 Javascript
Node.js 回调函数实例详解
2017/07/06 Javascript
将jquery.qqFace.js表情转换成微信的字符码
2017/12/01 jQuery
如何根据业务封装自己的功能组件
2019/04/19 Javascript
Node.js fs模块(文件模块)创建、删除目录(文件)读取写入文件流的方法
2019/09/03 Javascript
十分钟教你上手ES2020新特性
2020/02/12 Javascript
Sublime Text3 配置 NodeJs 环境的方法
2020/05/20 NodeJs
Windows8下安装Python的BeautifulSoup
2015/01/22 Python
python try 异常处理(史上最全)
2019/03/07 Python
python将类似json的数据存储到MySQL中的实例
2019/07/12 Python
Python3 全自动更新已安装的模块实现
2020/01/06 Python
HTML5 video视频字幕的使用和制作方法
2018/05/03 HTML / CSS
HTML5 b和i标记将被赋予真正的语义
2009/07/16 HTML / CSS
鞋子女王塔玛拉·梅隆同名奢侈品牌:Tamara Mellon
2017/11/22 全球购物
Opodo英国旅游网站:预订廉价航班、酒店和汽车租赁
2018/07/14 全球购物
Anthropologie英国:美国家喻户晓的休闲服装和家居产品品牌
2018/12/05 全球购物
奥地利智能家居和智能生活网上商店:tink.at
2019/10/07 全球购物
在C语言中"指针和数组等价"到底是什么意思?
2014/03/24 面试题
易程科技软件测试笔试
2013/03/24 面试题
儿科护理实习自我鉴定
2013/09/19 职场文书
小学学校门卫岗位职责
2014/08/03 职场文书
中队活动总结
2014/08/27 职场文书
勤俭节约主题班会
2015/08/13 职场文书
2016年第32个教师节红领巾广播稿
2015/12/18 职场文书
golang slice元素去重操作
2021/04/30 Golang