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

与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

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

(0)

相关推荐

  • React Hooks的深入理解与使用

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗? --拥有了Hooks,生命周期钩子函数可以先丢一边了. 你在还在为组件中的this指向而晕头转向吗? --既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this. 这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张.如果你也对react

  • React 使用Hooks简化受控组件的状态绑定

    开始之前 阅读本文需要对以下几项有一定了解 ECMAScript 6 文章中大量用到了 ES6 语法,比如解构赋值和函数参数默认值.剩余参数.展开语法.箭头函数等. Hooks React 在 16.8 版本中推出了 Hooks,它允许你在"函数组件"中使用"类组件"的一些特性. React 本身提供了一些 Hooks,比如 useState.useReducer 等.通过在一个以"use"作为命名起始的函数中调用这些 Hooks,就得到了一个

  • 记录一次完整的react hooks实践

    写在前面 React在16.8版本正式发布了Hooks.关注了很久,最近正好有一个小需求,赶紧来试一下. 需求描述 需求很简单,部门内部的一个数据查询小工具.大致长成下面这样: 用户首次访问页面,会拉取数据展示.输入筛选条件,点击查询后,会再次拉取数据在前端展示. 需求实现 使用React Class Component的写法 如果使用以前的class写法,简单写一下,代码可能大概长成下面这样: import React from 'react'; import { Tabs, Input, R

  • 如何对react hooks进行单元测试的方法

    写在前面 使用 react hook 来做公司的新项目有一段时间了,大大小小的坑踩了不少.由于是公司项目,因此必须要编写单元测试来确保业务逻辑的正确性以及重构时代码的可维护性与稳定性,之前的项目使用的是 react@15.x 的版本,使用 enzyme 配合 jest 来做单元测试毫无压力,但新项目使用的是 react@16.8 ,编写单元测试的时候,遇到不少阻碍,因此总结此篇文章算作心得分享出来. 配合 enzyme 来进行测试 首先,enzyme 对于 hook 的支持程度,可以参考这个 i

  • 基于Webpack4和React hooks搭建项目的方法

    面对日新月异的前端,我表示快学不动了:joy:. Webpack 老早就已经更新到了 V4.x,前段时间 React 又推出了 hooks API.刚好春节在家里休假,时间比较空闲,还是赶紧把 React 技术栈这块补上. 网上有很多介绍 hooks 知识点的文章,但都比较零碎,基本只能写一些小 Demo .还没有比较系统的,全新的基于 hooks 进行搭建实际项目的讲解.所以这里就从开发实际项目的角度,搭建起单页面 Web App 项目的基本脚手架,并基于 hooks API 实现一个 rea

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

    你还在为该使用无状态组件(Function)还是有状态组件(Class)而烦恼吗? --拥有了hooks,你再也不需要写Class了,你的所有组件都将是Function. 你还在为搞不清使用哪个生命周期钩子函数而日夜难眠吗? --拥有了Hooks,生命周期钩子函数可以先丢一边了. 你在还在为组件中的this指向而晕头转向吗? --既然Class都丢掉了,哪里还有this?你的人生第一次不再需要面对this. 这样看来,说React Hooks是今年最劲爆的新特性真的毫不夸张.如果你也对react

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

    与React类组件相比,React函数式组件究竟有何不同? 一般的回答都是: 类组件比函数式组件多了更多的特性,比如 state,那如果有 Hooks 之后呢? 函数组件性能比类组件好,但是在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别. 性能主要取决于代码的作用,而不是选择函数式还是类组件.尽管优化策略有差别,但性能差异可以忽略不计. 参考官网:(https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-b

  • React hooks useState异步问题及解决

    目录 React Hooks useState异步问题 原因 解决方法 React中useState异步更新小坑 问题点 React Hooks useState异步问题 最近在开发中遇到一个问题 我接口请求回来的数据 用useState存储起来. 但是我后面 去改变这个数据的时候每次拿到都是上次的数据没办法及时更新. 原因 useState 返回的更新状态方法是异步的,要在下次重绘才能获取新值.不要试图在更改状态之后立马获取状态. 解决方法 应该使用useRef 存储这个数据,在useEffe

  • React使用useEffect解决setState副作用详解

    目录 介绍一下API fetch()方法访问API setState的副作用 使用useEffect解决这个问题 使用useEffect操控函数运行 介绍一下API 本文主要内容:描述了setState与fetch之间产生的冲突副作用,并使用useEffect进行解决 API,即Application Programming Interface,应用程序接口,是很多程序向开发人员提供的易于使用的抽象化的代码. 比如经常会用到的查询天气API,智能识图API,如果是直接照着复杂的代码编写,会相当不

  • React TypeScript 应用中便捷使用Redux Toolkit方法详解

    目录 前言 背景 Redux-Toolkit 常规使用 优化方案 优化 useDispatch 和 useSelector 优化修改 redux 状态的步骤 总结 前言 本文介绍的主要内容是 Redux-Toolkit 在 React + TypeScript 大型应用中的实践,主要解决的问题是使用 createSlice 的前提下消费 redux 状态仍旧有点繁琐的问题. 阅读本文需要的前置知识:了解 React .Redux-Toolkit .TypeScript 的使用. 关于 Redux

  • React项目使用ES6解决方案及JSX使用示例详解

    目录 不使用 ES6 绑定 JSX如何? 不使用 ES6 然而,在纯浏览器端使用ES6语法时,浏览器支持存在差异,这需要特殊处理才能正常运行. 支持ES2015桌面浏览器 Chrome:从51版开始,它可以支持ES6 97%的新功能. Firefox:53版本支持97%的ES6新功能. Safari:从版本10开始,ES6 99%的新功能都可以得到支持. IE:Edge 15可以支持96%的ES6新功能. Edge 14可以支持93%的新ES6功能.(IE7~11基本不支持ES6) class

  • jQuery 跨域访问解决原理案例详解

    浏览器端跨域访问一直是个问题,多数研发人员对待js的态度都是好了伤疤忘了疼,所以病发的时候,时不时地都要疼上一疼.记得很久以前使用iframe 加script domain 声明.yahoo js util 的方式解决二级域名跨域访问的问题. 时间过得好快,又被拉回js战场时, 跨域问题这个伤疤又开疼了.好在,有jQuery帮忙,跨域问题似乎没那么难缠了.这次也借此机会对跨域问题来给刨根问底,结合实际的开发项目,查阅了相关资料,算是解决了跨域问题...有必要记下来备忘, 跨域的安全限制都是指浏览

  • PyCharm License Activation激活码失效问题的解决方法(图文详解)

    自己的小Python项目好几天没有写了,今天打开PyCharm准备继续写,突然发现之前的激活码被取消不能用了,本来激情满满的准备干活啦!之前搜的这个激活码本来说的是可以用到18年1月份的呢,哎-心情是崩溃的-如下图所示: 在经过一顿搜索挣扎之后,笔者终于找到解决方法,血条瞬间满了,满血复活有没有,开开心心的开始敲代码: 解决方法是在下面的网站找到的,我只是个搬运工,来具体写一下步骤,解决一下网站中网友的问题,网址如下:http://idea.lanyus.com/ 很多网友提问说,用了这里的激活

  • idea+git合并分支解决冲突及详解步骤

    Git分支详解参考: 分支管理组成 1.1.master主干 在版本管理中,代码库应该仅有一个主干.此主干是和当前生产保持一致的,是可用的.稳定的可直接发布的版本,不能再主干上进行任何开发操作.git主干的名字,默认叫做 master,它是自动建立的. 1.2.develop主开发分支 因为不能在主干master上进行开发,那么就需要在基于主干master的基础上,创建一个开发主分支develop,开发主分支develop的代码永远是最新的,所有的新功能都是以此分支为基础进行开发的,该分支只是做

  • java图论普利姆及克鲁斯卡算法解决最小生成树问题详解

    目录 什么是最小生成树? 普利姆算法  算法介绍 应用 --> 修路问题  图解分析  克鲁斯卡尔算法 算法介绍 应用场景 -- 公交站问题  算法图解   算法分析  如何判断是否构成回路 什么是最小生成树? 最小生成树(Minimum Cost Spanning Tree),简称MST. 最小生成树要求图是连通图.连通图指图中任意两个顶点都有路径相通,通常指无向图.理论上如果图是有向.多重边的,也能求最小生成树,只是不太常见. 普利姆算法  算法介绍 应用 --> 修路问题  图解分析 

  • 如何在IDEA中快速解决Jar冲突详解

    目录 一.为什么会产生Jar包冲突? 1.1 直接与传递依赖 1.2 Maven 的传递依赖 1.3 Maven 如何解决版本冲突? 1.4 覆盖传递依赖版本 1.5 使用直接依赖覆盖传递依赖版本 二.通过IDEA快捷解决依赖冲突 2.1 查找冲突 2.2 发现冲突 2.3 解决冲突 一.为什么会产生Jar包冲突? 作为 Java 开发人员,我们可能会使用 Maven 维护许多应用程序以进行依赖项管理.这些应用程序需要不时升级以保持最新状态并添加新功能或安全更新. 由于某些依赖项之间的冲突,这个

随机推荐