看完这篇,你也能把 React Hooks 玩出花

本文首发于政采云前端团队博客:看完这篇,你也能把 React Hooks 玩出花

https://www.zoo.team/article/react-hooks


本文中出现的部分名称映射:
函数式组件  => Function Component
类组件 => Class Component
工具函数 => Util Function
钩子 => React Hook
初始值 => initialValue

先讲概念

React v16.7.0-alpha 中第一次引入了 Hooks 的概念,在 v16.8.0 版本被正式发布。React Hooks 在 React 中只是对 React Hook 的概念性的描述,在开发中我们用到的实际功能都应该叫做 React hook
React Hook 是一种特殊的函数,其本质可以是函数式组件(返回 Dom 或 Dom 及 State ),也可以只是一个工具函数(传入配置项返回封装后的数据处理逻辑)。

再总结

React Hooks 的出现使函数式组件变得焕然一新,其带来的最大的变化在于给予了函数式组件类似于类组件 生命周期的概念,扩大了函数式组件的应用范围。
目前函数式组件基本用于纯展示组件,一旦函数式组件耦合有业务逻辑,就需要通过 Props 的传递,通过子组件触发父组件方法的方式来实现业务逻辑的传递,Hooks 的出现使得函数组件也有了自己的状态与业务逻辑,简单逻辑在自己内部处理即可,不再需要通过 Props 的传递,使简单逻辑组件抽离更加方便,也使使用者无需关心组件内部的逻辑,只关心 Hooks 组件返回的结果即可。
在我看来,Hooks 组件的目标并不是取代类组件,而是增加函数式组件的使用率,明确通用工具函数与业务工具函数的边界, 鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数
之所以把总结放在前面,是想让大家在看后面的内容时有一个整体的概念去引导大家去思考 React Hooks 具体给函数式组件带来了什么变化。

Hooks 初识

官方提供的钩子

目前官方提供的钩子共分为两种,分为基本钩子以及拓展钩子
基本钩子共有:
useState 、useEffect 、 useContext
额外的钩子有:

useRef 、useCallback 、useMemo 、

useReducer 、useLayoutEffect 、

useImperativeHandle 、useDebugValue

不同钩子用法

useState
该钩子用于创建一个新的状态,参数为一个固定的值或者一个有返回值的方法。钩子执行后的结果为一个数组,分别为生成的状态以及改变该状态的方法,通过解构赋值的方法拿到对应的值与方法。
使用方法如下:

 
    
export  default  function HookDemo({
   const [count, changeCount] = useState( 0);

   return (
     <div>
      {count}
      <button onClick={() => { changeCount(Math.ceil(Math.random() * 1000)); }}>
        改变count
      button>

    div>
  );
}
看完这篇,你也能把 React Hooks 玩出花_第1张图片

useEffect
顾名思义,执行副作用钩子。主要用于以下两种情况:
  1. 函数式组件中不存在传统类组件生命周期的概念,如果我们需要在一些特定的生命周期或者值变化后做一些操作的话,必须借助   useEffect  的一些特性去实现。
  2. useState  产生的 changeState 方法并没有提供类似于   setState  的第二个参数一样的功能,因此如果需要在 State 改变后执行一些方法,必须通过   useEffect  实现。
该钩子接受两个参数,第一个参数为副作用需要执行的回调,生成的回调方法可以返回一个函数( 将在组件卸载时运行);第二个为该副作用监听的状态数组,当对应状态发生变动时会执行副作用, 如果第二个参数为空,那么在每一个 State 变化时都会执行该副作用。
使用方法如下:

 
    
const [count, changeCount] = useState( 0);

// 将在count变化时打印最新的count数据
useEffect( () => {
  message.info( `count发生变动,最新值为${count}`);
}, [count])
看完这篇,你也能把 React Hooks 玩出花_第2张图片

在上面代码中我们实现了在   useEffect  这个钩子适用情况中的第二种情况,那么如何使用该钩子才能实现类似于类组件中生命周期的功能呢?既然第一个参数是副作用执行的回调,那么实现我们所要功能的重点就应该在第二个参数上了。
componentDidMount  &&   componentWillUnmout:这两个生命周期只在页面挂载/卸载后执行一次。前面讲过,所有的副作用在组件挂载完成后会执行一次 ,如果副作用存在返回函数,那么返回的函数将在卸载时运行。借助这样的特性,我们要做的就是让目标副作用在初始化执行一次后再也不会被调用,于是只要让与该副作用相关联的状态为空,不管其他状态如何变动,该副作用都不会再次执行,即实现了   componentDidMount  与   componentWillUnmout

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

function Child({ visible }{
  useEffect( () => {
    message.info( '我只在页面挂载时打印');
     return  () => {
      message.info( '我只在页面卸载时打印');
    };
  }, []);

   return visible ?  'true' :  'false';
}

export  default  function HookDemo({
   const [visible, changeVisible] = useState( true);


   return (
     <div>
      {
        visible && <Child visible={visible} />
      }
      <button onClick={() => { changeVisible(!visible); }}>
        改变visible
      button>

    div>
  );
}
看完这篇,你也能把 React Hooks 玩出花_第3张图片

componentDidUpdate:该生命周期在每次页面更新后都会被调用。那么按照之前的逻辑,就应该把所有的状态全部放在第二个状态中,但是这样的话新增/删除一个状态都需要改变第二参数。其实, 如果第二个参数为空,那么在每一个 State 变化时都会执行该副作用,那么如果要实现   componentDidUpdate  就非常简单了。

 
    
useEffect( () => {
   // ...副作用逻辑
})  // 注意上面说的关联状态为空不是说不传递第二个参数,而是第二个参数应该为一个空数组
看完这篇,你也能把 React Hooks 玩出花_第4张图片


在类组件中,如果在   componentDidMount  中多次调用   setState  设置一个值( 当然不推荐这样做),并在成功的回调中打印该值,那么最后的结果很可能会打印很多个相同的最后一次设置的值。是因为类的 setState  是一个类异步的结果,他们会将所有变动的内容进行收集然后在合适的时间去统一赋值。
而在   useEffect  中,所有的变量的值都会保留在该副作用执行的时刻,类似于 for 循环中的 let 或者 闭包,所有的变量都维持在副作用执行时的状态,也有人称这个为 Capture Value。
useCallback
生成 Callback 的钩子。用于对不同   useEffect  中存在的相同逻辑的封装,减少代码冗余,配合   useEffect   使用。
该钩子先看例子会比较好理解一下:

 
    
const [count1, changeCount1] = useState( 0);
const [count2, changeCount2] = useState( 10);

const calculateCount = useCallback( () => {
   if (count1 && count2) {
     return count1 * count2;
  }
   return count1 + count2;
}, [count1, count2])

useEffect( () => {
     const result = calculateCount(count, count2);
    message.info( `执行副作用,最新值为${result}`);
}, [calculateCount])
看完这篇,你也能把 React Hooks 玩出花_第5张图片
在上面的例子中我们通过   useCallback  的使用生成了一个回调, useCallback  的使用方法和   useEffect  一致,第一个参数为生成的回调方法,第二个参数为该方法关联的状态, 任一状态发生变动都会重新生成新的回调
通过上面代码的使用,我们将 count1 / count2 的值与一个叫做 calculateCount 的方法关联了起来,如果组件的副作用中用到计算 count1 和 count2 的值的地方,直接调用该方法即可。
其中和直接使用   useEffect  不同的地方在于使用   useCallback  生成计算的回调后,在使用该回调的副作用中, 第二个参数应该是生成的回调。其实这个问题是很好理解的,我们使用   useCallback  生成了一个与 count1 / count2 相关联的回调方法,那么当关联的状态发生变化时会重新生成新的回调,副作用监听到了回调的变化就会去重新执行副作用, 此时  useCallback  和 useEffect 是按顺序执行的, 这样就实现了副作用逻辑的抽离。
useRef
useRef  接受一个参数,为 ref 的初始值。类似于类组件中的   createRef  方法 ,该钩子会返回一个对象,对象中的 current 字段为我们 指向的实例 / 保存的变量,可以实现获得目标节点实例或保存状态的功能。
useRef 保存的变量不会随着每次数据的变化重新生成,而是保持在我们最后一次赋值时的状态,依靠这种特性,再配合 useCabllback  和  useEffect 我们可以实现 preProps/preState 的功能。

 
    
const [count, changeCount] = useState( 0);
const [count1, changeCount1] = useState( 0);
// 创建初始值为空对象的prestate
const preState = useRef({});
// 依赖preState进行判断时可以先判断,最后保存最新的state数据
useEffect( () => {
   const { ... } = preState.current;
   if ( // 条件判断) {
     // 逻辑
  }
   // 保存最新的state
  preState.current = {
    count,
    count1,
  }
});
看完这篇,你也能把 React Hooks 玩出花_第6张图片

另外,当我们将使用   useState 创建的状态赋值给   useRef  用作初始化时, 手动更改 Ref 的值并不会引起关联状态的变动。从该现象来看, useRef 似乎只是在内存空间中开辟了一个堆空间将初始化的值存储起来,该值与初始化的值存储在不同的内存空间,修改 Ref 的值不会引起视图的变化。

 
    
export  default  function HookDemo({
   const [count] = useState({  count1 });

   const countRef = useRef(count);

   return (
     <div>
      {count.count}
      <button onClick={() => { countRef.current.count = 10; }}>
        改变ref
      button>

    div>
  );
}
看完这篇,你也能把 React Hooks 玩出花_第7张图片
useRef正常
useMemo
Memo 为 Memory 简写, useMemo 即使用记忆的内容。该钩子主要用于做性能的优化。
前面我们说过了当状态发生变化时,没有设置关联状态的   useEffect  会全部执行。同样的, 通过计算出来的值或者引入的组件也会重新计算/挂载一遍,即使与其关联的状态没有发生任何变化
在类组件中我们有 
  shouldComponetUpdate  以及   React.memo  
帮助我们去做性能优化,如果在函数组件中没有类似的功能显示是违背了官方的初衷的,于是就有了   useMemo  这个钩子。
在业务中,我们可以用   useMemo  来处理计算结果的缓存或引入组件的防止重复挂载优化。其接受两个参数,第一个参数为一个 Getter 方法, 返回值为要缓存的数据或组件,第二个参数为该返回值相关联的状态,当其中任何一个状态发生变化时就会重新调用 Getter 方法生成新的返回值。
具体代码如下:

 
    
import React, { useState, useMemo }  from  'react';
import { message }  from  'antd';

export  default  function HookDemo({
   const [count1, changeCount1] = useState( 0);
   const [count2, changeCount2] = useState( 10);

   const calculateCount = useMemo( () => {
    message.info( '重新生成计算结果');
     return count1 *  10;
  }, [count1]);
   return (
     <div>
      {calculateCount}
      <button onClick={() => { changeCount1(count1 + 1); }}>改变count1button>

      <button onClick={() => { changeCount2(count2 + 1); }}>改变count2button>
    div>
  );
}
看完这篇,你也能把 React Hooks 玩出花_第8张图片

初次接受   useMemo  时可能我们会觉得该钩子只是用来做计算结果的缓存,返回值只能是一个数字或字符串。其实   useMemo  并不关心我们的返回值类型是什么,它只是在关联状态发生变动时重新调用我们传递的 Getter 方法 生成新的返回值,也就是说   useMemo  生成的是 Getter 方法与依赖数组的关联关系。因此,如果我们将函数的返回值替换为一个组件,那么就可以 实现对组件挂载/重新挂载的性能优化
代码如下:

 
    
import React, { useState, useMemo }  from  'react';
import { message }  from  'antd';

function Child({ count }{
   return  <p>当前传递的count为:{count}p>;
}

export  default  function HookDemo({
   const [count1, changeCount1] = useState( 0);
   const [count2, changeCount2] = useState( 10);

   const child = useMemo( () => {
    message.info( '重新生成Child组件');
     return  <Child count={count1} />;
  }, [count1]);
  return (
    <div>
      {child}
      <button onClick={() => { changeCount1(count1 + 1); }}>改变count1button>

      <button onClick={() => { changeCount2(count2 + 1); }}>改变count2button>
    div>
  );
}
看完这篇,你也能把 React Hooks 玩出花_第9张图片

其他钩子
今天主要讲了组件中常用的几个钩子,剩下的未讲解的钩子中,如
useLayoutEffect  useImperativeHandle  useDebugValue ,
其功能都比较简单就不在此赘述。
还有一个比较重要的钩子   useContext,是 createContext 功能在函数式组件中的实现。通过该功能可以实现很多强大的功能,可以是说官方的 Redux,很多人对此应该有不少的了解。该钩子内容太多,后续单独使用一个章节进行描述。

编写自己的钩子

其实从上面讲解的内容来看,钩子并不是什么高深莫测的东西,它只是对我们常用逻辑的一些封装,接下来就会通过具体的代码来教大家写一个自己的钩子。

最基本的钩子

最基本的钩子也就是返回包含了更多逻辑的 State 以及改变 State 方法的钩子。拿计数器来说,其最基本的就是返回当前的数字以及减少/增加/重置等功能,明确完功能后可以开始动手做了。

 
    
import React, { useState }  from  'react';

// 编写我们自己的hook,名字以use开头
function useCounter(initialValue{
   // 接受初始化的值生成state
   const [count, changeCount] = useState(initialValue);
   // 声明减少的方法
   const decrease =  () => {
    changeCount(count -  1);
  }
   // 声明增加的方法
   const increase =  () => {
    changeCount(count +  1);
  }
   // 声明重置计数器方法
   const resetCounter =  () => {
    changeCount( 0);
  }
   // 将count数字与方法返回回去
   return [count, { decrease, increase, resetCounter }]
}

export  default  function myHooksView({
   // 在函数组件中使用我们自己编写的hook生成一个计数器,并拿到所有操作方法的对象
   const [count, controlCount] = useCounter( 10);
   return (
       <div>
        当前数量:{count}
            <button onClick={controlCount.decrease}>减少button>

            <button onClick={controlCount.increase}>增加button>
            <button onClick={controlCount.resetCounter}>重置button>
    div>
  )
}
在上面的例子中,我们将在   useCounter  这个钩子中创建了一个关联了   initialValue  的状态,并创建减少/增加/重置的方法,最终将其通过   return  返回出去。这样在其他组件需要用到该功能的地方,通过调用该方法拿到其返回值,即可实现对   useCounter  组件封装逻辑的复用。
演示效果如图:
看完这篇,你也能把 React Hooks 玩出花_第10张图片

返回 DOM 的钩子

返回 DOM 其实和最基本的 Hook 逻辑是相同的,只是在返回的数据内容上有一些差异,具体还是看代码,以一个 Modal 框为例。

 
    
import React, { useState }  from  'react';
import { Modal }  from  'antd';

function useModal({
   const [visible, changeVisible] = useState( false);

   const toggleModalVisible =  () => {
    changeVisible(!visible);
  };

   return [(
     <Modal
      visible={visible}
      onOk={toggleModalVisible}
      onCancel={toggleModalVisible}
    >

      弹窗内容
      Modal>

  ), toggleModalVisible];
}

export  default  function HookDemo({
   const [modal, toggleModal] = useModal();
   return (
     <div>
      {modal}
      <button onClick={toggleModal}>打开弹窗button>

    div>
  );
}

这样我们就实现了一个返回了弹窗内容以及改变弹窗显示状态的 Hook,其实可以封装的内容还有很多很多,可以通过配置项的设置实现更丰富的封装。
演示效果如图:
看完这篇,你也能把 React Hooks 玩出花_第11张图片

钩子/最终总结

钩子总结

看完这篇,你也能把 React Hooks 玩出花_第12张图片


从上面的表格中我们可以看出,在官方提供的 Hook 中,除了基本的  useState  与  useRef  外,其他钩子都存在第二个参数,第一个方法的执行与第二个参数相互关联。于是我们可以得出一个结论,在使用了 Hook 的函数式组件中,我们在使用 副作用/引用子组件时都需要时刻注意对代码进行性能上的优化

最终总结

我在前面的总结里是这么评价 React Hooks 的:
Hooks 组件的目标并不是取代 class component 组件,而是增加函数式组件的使用率,明确通用工具函数与业务工具函数的边界, 鼓励开发者将业务通用的逻辑封装成 React Hooks 而不是工具函数
希望看完这篇文章的你也有自己的一些看法,欢迎拍砖讨论。

招贤纳士

招人,前端,隶属政采云前端大团队(ZooTeam),50 余个小伙伴正等你加入一起浪~ 如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5年工作时间3年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手参与一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给  [email protected]

640?wx_fmt=png


640?wx_fmt=gif


你可能感兴趣的:(看完这篇,你也能把 React Hooks 玩出花)