react hooks理解

1、react hooks出现的动机?

1.1、在组件之间复用状态逻辑很难

React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如render props高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。这使得在组件间或社区内共享 Hook 变得更便捷。

在官网上有一个聊天程序例子,该程序中FriendStatus组件用于显示好友状态

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);       
  useEffect(() => {    
    function handleStatusChange(status) {      
        setIsOnline(status.isOnline);    
    }    
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);    
    return () => {      
        ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);    
    };  
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

现在我们假设聊天应用中有一个联系人列表,当用户在线时需要把名字设置为绿色。我们可以把上面类似的逻辑复制并粘贴到FriendListItem组件中来,但这并不是理想的解决方案:

import React, { useState, useEffect } from 'react';

function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  return (
    
  • {props.friend.name}
  • ); }

    相反,我们希望在FriendStatusFriendListItem之间共享逻辑。
    目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑:render props(指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术)和高阶组件(高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。高阶组件是参数为组件,返回值为新组件的函数),现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的。

    当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

    自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。例如,下面的useFriendStatus是我们第一个自定义的 Hook:

    import { useState, useEffect } from 'react';
    
    function useFriendStatus(friendID) {
      const [isOnline, setIsOnline] = useState(null);
    
      useEffect(() => {
        function handleStatusChange(status) {
          setIsOnline(status.isOnline);
        }
    
        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
      });
    
      return isOnline;
    }

    此处并未包含任何新的内容——逻辑是从上述组件拷贝来的。与组件中一致,请确保只在自定义 Hook 的顶层无条件地调用其他 Hook。

    与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话)。换句话说,它就像一个正常的函数。但是它的名字应该始终以use开头,这样可以一眼看出其符合Hook 的规则

    此处useFriendStatus的 Hook 目的是订阅某个好友的在线状态。这就是我们需要将friendID作为参数,并返回这位好友的在线状态的原因。

    现在我们已经把这个逻辑提取到useFriendStatus的自定义 Hook 中,然后就可以_使用它了:_

    function FriendStatus(props) {
      const isOnline = useFriendStatus(props.friend.id);
      if (isOnline === null) {
        return 'Loading...';
      }
      return isOnline ? 'Online' : 'Offline';
    }
    function FriendListItem(props) {
      const isOnline = useFriendStatus(props.friend.id);
      return (
        
  • {props.friend.name}
  • ); }

    这段代码等价于原来的示例代码吗?等价,它的工作方式完全一样。如果你仔细观察,你会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

    自定义 Hook 必须以 “use” 开头吗?必须如此。这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了Hook 的规则

    在两个组件中使用相同的 Hook 会共享 state 吗?不会。自定义 Hook 是一种重用_状态逻辑_的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

    自定义 Hook 如何获取独立的 state?每次_调用_Hook,它都会获取独立的 state。由于我们直接调用了useFriendStatus,从 React 的角度来看,我们的组件只是调用了useStateuseEffect。 正如我们在之前章节了解到的一样,我们可以在一个组件中多次调用useStateuseEffect,它们是完全独立的。

    1.2、复杂组件变得难以理解

    我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在componentDidMountcomponentDidUpdate中获取数据。但是,同一个componentDidMount中可能也包含很多其它的逻辑,如设置事件监听,而之后需在componentWillUnmount中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

    在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

    为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

    在显示好友是否在线的FriendStatus组件中,从 class 中 props 读取friend.id,然后在组件挂载后订阅好友的状态,并在卸载组件的时候取消订阅:

      componentDidMount() {
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }

    但是当组件已经显示在屏幕上时,friend prop 发生变化时会发生什么?我们的组件将继续展示原来的好友状态。这是一个 bug。而且我们还会因为取消订阅时使用错误的好友 ID 导致内存泄露或崩溃的问题。

    在 class 组件中,我们需要添加componentDidUpdate来解决这个问题:

      componentDidMount() {
        ChatAPI.subscribeToFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }
    
      componentDidUpdate(prevProps) {    
          // 取消订阅之前的 friend.id    
          ChatAPI.unsubscribeFromFriendStatus(      
              prevProps.friend.id,      
              this.handleStatusChange    
          );    
          // 订阅新的 friend.id    
          ChatAPI.subscribeToFriendStatus(      
              this.props.friend.id,      
              this.handleStatusChange    
          );  
      }
      componentWillUnmount() {
        ChatAPI.unsubscribeFromFriendStatus(
          this.props.friend.id,
          this.handleStatusChange
        );
      }

    忘记正确地处理componentDidUpdate是 React 应用中常见的 bug 来源。

    现在看一下使用 Hook 的版本:

    function FriendStatus(props) {
      // ...
      useEffect(() => {
        // ...
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
          ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
      });

    它并不会受到此 bug 影响。(虽然我们没有对它做任何改动。)

    并不需要特定的代码来处理更新逻辑,因为useEffect_默认_就会处理。它会在调用一个新的 effect 之前对前一个 effect 进行清理。为了说明这一点,下面按时间列出一个可能会产生的订阅和取消订阅操作调用序列:

    // Mount with { friend: { id: 100 } } props
    ChatAPI.subscribeToFriendStatus(100, handleStatusChange);     // 运行第一个 effect
    
    // Update with { friend: { id: 200 } } props
    ChatAPI.unsubscribeFromFriendStatus(100, handleStatusChange); // 清除上一个 effect
    ChatAPI.subscribeToFriendStatus(200, handleStatusChange);     // 运行下一个 effect
    
    // Update with { friend: { id: 300 } } props
    ChatAPI.unsubscribeFromFriendStatus(200, handleStatusChange); // 清除上一个 effect
    ChatAPI.subscribeToFriendStatus(300, handleStatusChange);     // 运行下一个 effect
    
    // Unmount
    ChatAPI.unsubscribeFromFriendStatus(300, handleStatusChange); // 清除最后一个 effect

    此默认行为保证了一致性,避免了在 class 组件中因为没有处理更新逻辑而导致常见的 bug。

    1.3、难以理解的 class

    除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中this的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。

    export default App extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                count: 0
            }
        }
        
        handle() {
            this.setState((preState) => ({
               count: preState.count + 1 
            }))
        }
        
        render() {
            return 
        }
    }

    使用函数组件就不用理会那些this指针了

    const App = props => {
        const [count, setCount] = useState(0)
        
        return 
    }

    2、react hooks怎么用?

    官网介绍非常详细,链接:https://react.docschina.org/d...

    3、理解useState、useEffect

    3.1、useState

    1.最简单的 useState 用法是这样的:
    demo1:https://codesandbox.io/s/v0nqm309q3

    function Counter() {
      var [count, setCount] = useState(0);
    
      return (
        
    {count}
    ); }

    2.基于 useState 的用法,我们尝试着自己实现一个 useState:
    demo2:https://codesandbox.io/s/myy5qvoxpp

    function useState(initialValue) {
      var state = initialValue;
      function setState(newState) {
        state = newState;
        render();
      }
      return [state, setState];
    }

    3.这时我们发现,点击 Button 的时候,count 并不会变化,为什么呢?我们没有存储 state,每次渲染 Counter 组件的时候,state 都是新重置的。

    自然我们就能想到,把 state 提取出来,存在 useState 外面。

    demo3:https://codesandbox.io/s/q9wq6w5k3w

    var _state; // 把 state 存储在外面
    
    function useState(initialValue) {
      _state = _state || initialValue; // 如果没有 _state,说明是第一次执行,把 initialValue 复制给它
      function setState(newState) {
        _state = newState;
        render();
      }
      return [_state, setState];
    }

    到目前为止,我们实现了一个可以工作的 useState,至少现在来看没啥问题。

    接下来,让我们看看 useEffect 是怎么实现的。

    3.2、useEffect

    useEffect 是另外一个基础的 Hook,用来处理副作用,最简单的用法是这样的:

    demo4:https://codesandbox.io/s/93jp55qyp4

    useEffect(() => {
        console.log(count);
    }, [count]);

    我们知道 useEffect 有几个特点:

    1. 有两个参数 callback 和 dependencies 数组
    2. 如果 dependencies 不存在,那么 callback 每次 render 都会执行
    3. 如果 dependencies 存在,只有当它发生了变化, callback 才会执行

    我们来实现一个 useEffect

    demo5:https://codesandbox.io/s/3kv3zlvzl1

    let _deps; // _deps 记录 useEffect 上一次的 依赖
    
    function useEffect(callback, depArray) {
      const hasNoDeps = !depArray; // 如果 dependencies 不存在
      const hasChangedDeps = _deps
        ? !depArray.every((el, i) => el === _deps[i]) // 两次的 dependencies 是否完全相等
        : true;
      /* 如果 dependencies 不存在,或者 dependencies 有变化*/
      if (hasNoDeps || hasChangedDeps) {
        callback();
        _deps = depArray;
      }
    }

    到这里,我们又实现了一个可以工作的 useEffect,似乎没有那么难。

    此时我们应该可以解答一个问题:

    Q:为什么第二个参数是空数组,相当于componentDidMount

    A:因为依赖一直不变化,callback 不会二次执行。

    3.3、Not Magic, just Arrays

    到现在为止,我们已经实现了可以工作的 useState 和 useEffect。但是有一个很大的问题:它俩都只能使用一次,因为只有一个 _state 和 一个 _deps。比如

    const [count, setCount] = useState(0);
    const [username, setUsername] = useState('fan');

    count 和 username 永远是相等的,因为他们共用了一个 _state,并没有地方能分别存储两个值。我们需要可以存储多个 _state 和 _deps。

    如 《React hooks: not magic, just arrays》所写,我们可以使用数组,来解决 Hooks 的复用问题。

    demo6:https://codesandbox.io/s/50ww35vkzl

    代码关键在于:

    1. 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
    2. 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。
    3. 如果还是不清楚,可以看下面的图。
    let memoizedState = []; // hooks 存放在这个数组
    let cursor = 0; // 当前 memoizedState 下标
    
    function useState(initialValue) {
      memoizedState[cursor] = memoizedState[cursor] || initialValue;
      const currentCursor = cursor;
      function setState(newState) {
        memoizedState[currentCursor] = newState;
        render();
      }
      return [memoizedState[cursor++], setState]; // 返回当前 state,并把 cursor 加 1
    }
    
    function useEffect(callback, depArray) {
      const hasNoDeps = !depArray;
      const deps = memoizedState[cursor];
      const hasChangedDeps = deps
        ? !depArray.every((el, i) => el === deps[i])
        : true;
      if (hasNoDeps || hasChangedDeps) {
        callback();
        memoizedState[cursor] = depArray;
      }
      cursor++;
    }

    我们用图来描述 memoizedState 及 cursor 变化的过程。

    3.3.1、 初始化


    react hooks理解_第1张图片

    3.3.2、 初次渲染

    react hooks理解_第2张图片

    3.3.3、 事件触发

    react hooks理解_第3张图片

    3.3.4、 Re Render

    react hooks理解_第4张图片

    到这里,我们实现了一个可以任意复用的 useState 和 useEffect。

    同时,也可以解答几个问题:

    Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。

    A:memoizedState 数组是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

    Q:自定义的 Hook 是如何影响使用它的函数组件的?

    A:共享同一个 memoizedState,共享同一个顺序。

    Q:“Capture Value” 特性是如何产生的?

    A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

    3.4、真正的 React 实现

    虽然我们用数组基本实现了一个可用的 Hooks,了解了 Hooks 的原理,但在 React 中,实现方式却有一些差异的。

    • React 中是通过类似单链表的形式来代替数组的。通过 next 按顺序串联所有的 hook。
      type Hooks = {
          memoizedState: any, // 指向当前渲染节点 Fiber
          baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
          baseUpdate: Update | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
          queue: UpdateQueue | null,// UpdateQueue 通过
          next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
        }
         
        type Effect = {
          tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
          create: () => mixed, // 初始化 callback
          destroy: (() => mixed) | null, // 卸载 callback
          deps: Array | null,
          next: Effect, // 同上 
        };
    • memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?

      我们知道,react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。

    react hooks理解_第5张图片

    参考:
    https://react.docschina.org/d...
    https://github.com/brickspert...
    https://juejin.im/post/5e53d9...
    https://juejin.im/post/5be3ea...

    你可能感兴趣的:(react-hooks,react.js)