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.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. 欢迎关注公众号「前端进阶课」认真学前端,一起进阶。

原文地址:https://www.cnblogs.com/zhongmeizhi/p/12201684.html

时间: 2024-10-25 13:07:09

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

理解 React Hooks

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由志航发表于云+社区专栏 TL;DR 一句话总结 React Hooks 就是在 react 函数组件中,也可以使用类组件(classes components)的 state 和 组件生命周期,而不需要在 mixin. 函数组件.HOC组件和 render props 之间来回切换,使得函数组件的功能更加实在,更加方便我们在业务中实现业务逻辑代码的分离和组件的复用. 本文将从以下几个方面介绍 hooks Hooks 在解决什么问题

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

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

react新特性 react hooks

本文介绍的是react新特性react hooks,本文面向的是有一定react开发经验的小伙伴,如果你对react还不是很熟悉的话我建议你先学习react并多多联系. 首先我们都知道react有3种组件:分别是Function函数式无状态组件.class有状态组件.高阶组件.这里不对这3种组件做一一介绍. 本文重点是react hooks 一个最简单的Hooks 首先让我们看一下一个简单的有状态组件: 1 class Example extends React.Component { 2 co

通过 React Hooks 声明式地使用 setInterval

本文由云+社区发表 作者:Dan Abramov 接触 React Hooks 一定时间的你,也许会碰到一个神奇的问题: setInterval 用起来没你想的简单. Ryan Florence 在他的推文里面说到: 不少朋友跟我提起,setInterval 和 hooks 一起用的时候,有种蛋蛋的忧伤. 老实说,这些朋友也不是胡扯.刚开始接触 Hooks 的时候,确实还挺让人疑惑的. 但我认为谈不上 Hooks 的毛病,而是 React 编程模型和 setInterval 之间的一种模式差异.

初探React Hooks &amp; SSR改造

Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨组件的状态管理变得非常困难,复用的组件也会因为要兼容不同的组件变得产生很多副作用,如果对组件再次拆分,也会造成冗余代码增多,和组件过多带来的问题. 后来有了 Redux 之类的状态管理库,来统一管理组件状态.但是这种分层依然会让代码变得很复杂,需要更多的嵌套.状态和方法,写代码时也常在几个文件之间不

React Hooks简单业务场景实战(非源码解读)

前言 React Hooks 是React 16.7.0-alpha 版本推出的新特性.从 16.8.0 开始,React更稳定的支持了这一新特性. 它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 注意:React 16.8.0 是第一个支持 Hook 的版本.升级时,请注意更新所有的 package,包括 React DOM.React Native 将在下一个稳定版本中支持 Hook. 如果说promise是JavaScript异步的终极解决方案,那

React Hooks究竟是什么呢?

摘要: React Hooks原理解析. 原文:快速了解 React Hooks 原理 译者:前端小智 我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件却不能? React 早期版本,类组件可以通过继承PureComponent来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以减少一些不必要的渲染,直接 16.6 出来的 React.memo函数. React 16.8 新出来的Hook可以让React 函数

React hooks能替代Redux,HOC和render props么?

最近开始学习React,记录一下心得. React hooks是16.8.0推出的,其目的是为了替换class,HOC,render props.同时网传hooks也将终结redux,那么本文将讨论hooks究竟能不能替换掉redux,HOC,render props. 1. Hooks替代Redux. 与其说Hooks替代Redux,不如说是Context在替代Redux.Context是16.3.0推出的(之前也有,但是API不一样且基本没人用).其实从Context开始,可以说不用redu

React Hooks用法大全

前言 在 React 的世界中,有容器组件和 UI 组件之分,在 React Hooks 出现之前,UI 组件我们可以使用函数,无状态组件来展示 UI,而对于容器组件,函数组件就显得无能为力,我们依赖于类组件来获取数据,处理数据,并向下传递参数给 UI 组件进行渲染.在我看来,使用 React Hooks 相比于从前的类组件有以下几点好处: 代码可读性更强,原本同一块功能的代码逻辑被拆分在了不同的生命周期函数中,容易使开发者不利于维护和迭代,通过 React Hooks 可以将功能代码聚合,方便