你真的会用react hooks?useEffect/useRef如何发请求、获取dom等问题)

前言

  看过几个react hooks 的项目,控制台上几百条警告,大多是语法不规范,react hooks 使用有风险,也有项目直接没开eslint。当然,这些项目肯定跑起来了,因为react自身或者其他的包,在编译的时候弥补了一些缺陷,还有一些是不规范的警告,或者还没运行到报错的代码。

  在这,我想分享并解析一些react开发过程中,一些很常见的需求,以及正确的用法,至少也得做到控制台没有任何警告才行。当然,如果大家有更好的方式,也请留言。

  接下来我会把这些问题做个汇总,请看目录。然后以我会以最常见的表格增删改查界面举例,配合代码做个讲解。

  每个问题都是我初学react hooks的时候,一步步踩过的坑,错误案例肯定没代码了,正确的自然有,源代码在我的GitHub上   点这里,看源码

   这篇博客我会一直更新,想到啥写啥,当做一个使用记录本用。 

 

目录

    1、useState如何合理的声明变量(合并优化state)

  2、组件加载后该如何发送 http 请求(useEffect)

  3、如何获取dom,并绑定监听事件(useRef)

  4、useEffect中用到了navigate、dispatch 或者其他库变量,导致多次执行怎么处理

  5、useMemo 什么时候用?怎么用?

1、useState如何合理的声明变量(这是个模糊的问题,没有所谓的唯一答案,所以我想通过一个案例分析过程,帮助你找到最科学的答案)

  在class时代,一个组件的所有state都在一个对象中。然而函数组件允许你分离声明,那现在我们是参考以前仅声明一个大state变量?还是每个变量都单独声明呢?

  显然,都不对。应该是一个折中的状态。具体如何找这个边界,我们先看下面的图片,通过一个例子让你理解。

     看图,这是一个很简单的场景,当点击查询,请求数据,修改页码也要请求数据。以前我们通常会声明2个属性 search 代表查询条件,pagination 代表页码,接下来按照这个思路试一下。

  a、第一次尝试(失败了)

  看代码,关于为什么用 useEffect 监听变量请求数据,我会在 目录第二条http请求 有详细说明,我们先看这个例子。

  首先,我们先看一下 useState 的用法。  const [变量,update更新函数] = useState(默认值);   这是一个标准的 useState 的用法。然后我们组织下代码,如下所示,执行一下。功能实现了,看起来没问题。但是看一下控制台的打印,点击搜索后,useEffect执行了2次,发了两次请求,这显然不符合预期。原因是useState的第二个结果 update 函数是异步执行的,看控制台会发现,pagination变化了,search还没变化。所以第一次请求只有页码,第二次请求才是正确的。显然这样写有问题。以前class组件,我们会在setState的会调中直接请求数据。但是函数组件不行,第一,我并没有发现update函数有回调。第二,多个update,总不能先更新search,回调里面再更新pagination,回调里面再请求数据。

let [search, setSearch] = useState({}),
     [pagination, setPagination] = useState({current: 1, pageSize: 3});

// 搜索
  const handleSearch = (d) => {
    setPagination({...pagination, current: 1});
    setSearch(d);
  };

useEffect(() => {
    const getData = () => {// 这是请求数据的函数};
    console.log(search)
    console.log(pagination)
    getData();
  }, [search, pagination]);

  b、第二次尝试(成功了,没有任何问题)

  分开声明失败了,那我们先合并成一个变量,试一下有没有问题。代码看下方。运行一下,发现没问题,控制台只打印一次,请求也只执行一次,非常nice。

 结论:

  现在我们捋一下这个问题,对于表格来说搜索条件和分页都是查询条件,配合使用,而且还存在同时修改的情况。除此之外这两个变量还分别需要给查询条件组件和表格页码属性使用。所以数据结构是不能改变的,但是他们的职能对表格来说是一样的。所以这样的变量就应该合到一起声明。分开反而造成很多麻烦。至此,我们明白,至少他俩是需要合并在一起声明的。

  那么有人会问,表格查询除了这两个变量,还会有 loading加载状态,表格数据,表头、数据总条数等等数据,要不要合并。还是那句话,分析它的功能和使用场景。我们做个变量变化的简单对比:

  请求数据之前:loading、查询条件、页码  可能变化

  请求数据之后:loading、表格数据、数据总数total  可能变化

  很显然,根据变化的时机就可以分为3种,如果强行把数据都凑在一起,一方面useEffect无法精细监听,另一方面,修改一个数据,需要解构所有数据。最合适的方案就是分3次useState声明。以小观大,其他变量也都是这样去分析的。以前class组件无脑定义,现在需要仔细分析每个变量。

 

let [search, setSearch] = useState({data: {}, pagination: {current: 1, pageSize: 3}});

// 搜索
  const handleSearch = (d) => {
    setSearch({
      data: {...d},
      pagination: {...search.pagination, current: 1}
    });
  };

useEffect(() => {
    const getData = () => {查询数据};
    console.log('---------useEffect----2-----')
    console.log(search)
    getData();
  }, [search]);

 

 

2、组件加载后该如何发送 http 请求

  相信很多人被react文档给坑过,看到文档 useEffect 的第二个参数传空数组,就只会触发一次,可以在这里发送http请求获取数据。哈哈哈!我一开始也被坑了,下边的代码和图片,就是错误的写法和控制台警告。但是,react源码其实是可以避免这种风险的,只是eslint不知道,这就导致写法不好,但是功能正常。所以后来react FAQ特地解释了一下,看下图。

// 查询表格数据
const getData = () => {};
useEffect(() => {
    getData();
  }, []);

 

 

 

 

 

 

   好吧,错误用法已经看过了,那究竟要怎么写才算正确的呢?现在我们已经可以确定,可以利用useEffect实现http请求,当然别的hooks也能实现。那么在讲解正确做法之前,得先看一段代码,你需要先理解 useEffect 到底是怎么执行的,都在什么时间执行。ps:接下来很关键,能让你彻底理解useEffect,特别是我用红色字体标记的3个地方。

  看代码,刷新界面,控制台的打印顺序应该是 1 2 3。刷新界面之后,函数组件执行,1打印了,然后执行return,渲染页面,再然后依次执行useEffect,所以 2  3 依次打印。所以useEffect会在dom渲染之后执行,而且是初始化的时候就会执行一次,这个时机就是 componentDidMount。然后,如果现在修改一下变量 c 的值,再看控制台,输出为 1 2,而且1的地方打印c的值是最新的值,也就说useEffect只会在监听的变量变化的时候,等dom渲染完了,再次触发。每次变化都是这样,这个时机就是  componentDidUpdate 。变量a 、b因为没有变化,自然就不执行。最后,如果你有其他页面,换到别的页面,再看控制台打印 2,只有2。如果你多放几个console,你会发现useEffect没有执行,函数组件也没有执行,只有getData这个方法执行了。因为我写了一个return getData; 什么意思呢,useEffect里面写return,就会在组件卸载之前,执行你return的函数,常用于卸载一些监听或者定时器等等。这个时机就是 componentWillUnmount

useEffect(() => {
   const getData = () => {
      console.log('----------2----------')
    };
    getData();
    return getData;
  }, [c]);
useEffect(()
=> { console.log('--------3---------', a, b) }, [a, b]);
console.log(
'-----1------',c);

return (<span>111</span>)

  大家看文档都知道class组件那么多生命周期,函数组件只有hooks,然后就不知道该怎么组织代码逻辑了。又或者知道useEffect能实现上述3个生命周期的功能,却不知道具体是怎么实现的。现在大家应该都知道useEffect怎么用了吧。接下来我们发http请求就简单多了。

  看代码,因为useEffect内部是一个闭包,内部使用的变量都必须显式的声明依赖,要不然就会报警告,缺少xxx依赖。所以我们直接将请求表格数据的方法声明在useEffect内部,然后调用,那他使用的所有的props或者state,都应该是他的依赖,需要写到中括号里面,否则都会报警告。首先代码这样写是没问题的,也是官方推荐的写法。这里先把正统确立了,然后打假。

  如果大家百度一下,网上可能还会有2种其他做法,useCallback 和 /* eslint-disable */。我将官方的回答截图放到下边,大家可以好好看看。推荐的方法就不多说什么了,useCallback 是在万不得已,实在没办法的时候才会使用。而第二种就更搞笑了,直接屏蔽eslint检测。当我搜到这种文章的时候,差点气到岔气,这是在掩耳盗铃吗。。。。(当然那种极其特殊需求的情况下可能确实需要屏蔽这个警告)至于官方说的另外2种方法,因为和请求无关,这里我就不多说了,大家可以在函数组件外声明一个变量,useEffect也是可以监听的,可以试一下。

  最后,言归正传,以前class组件的数据请求通常是靠回调触发,比如修改什么变量直接请求数据。现在不行,比如下边的写法,你需要确定这个请求需要的变量,而且这些变量的变化都是需要触发数据请求的。比如一个增删改查的界面,表格数据获取发生在初始化的时候、页码变化和点击查询的时候。所以,我们需要确保点击查询和修改页码一定改变变量,其他任何情况,不能修改变量,因为我们靠监听变量触发请求。

useEffect(() => {
    // 查询表格数据
    const getData = () => {
      setLoading(true);
      const { data, pagination } = search;
      const params = {
        ...data,
        current: pagination.current,
        size: pagination.pageSize
      }
      getList(params).then(res => {
        if (res.code === 200) {
          let d = res.data;
          setDataSource(d.records);
          setTotal(d.total)
        }
        setLoading(false);
      })
      .catch(err => {
        setLoading(false);
      });
    };
    getData();
  }, [search]);

 

3、如何获取dom,并绑定监听事件(useRef)

  这也是我们常见的需求,获取dom,然后动态 addEventListener 某些事件。实现这个功能我们使用useRef、useEffect两个hooks。

  首先,你需要知道 useRef 的三个特点。 第一,他声明的变量,将存活于组件的所有生命周期,注意是所有,组件注销,变量自动销毁; 第二,他可以存储任意类型变量,不仅仅是dom和普通对象; 第三,他声明的变量,数据类型是一个对象,对象上有current属性,赋值操作都在这个属性上进行,而且useRef声明的变量值变化了,不会引起函数组件的重新渲染,他只是一个存储数据的仓库,数据修改也是实时的。可以简单理解为react开辟了一块地址,专门用来存储你声明的变量,后续操作只是不断往这个地址换数据而已,组件注销,地址释放,都不需要我们额外操心。ps:我在想既然变量注销了,我真的还需要移除监听吗,这是我的疑问,不过我没法去印证这个事情。。。

  上面是useRef的特点,然后我们看代码,节约篇幅,我删掉了很多,源码在GitHub src\components\c-large-select  这个文件。其实这个功能简单,useRef声明变量,然后绑定到div标签的ref属性上,这样dom渲染之后 scrollEle.current 就可以拿到dom了,然后再useEffect中添加事件绑定,不懂的,看目录第二条,我详细介绍了useEffect的执行时机。这里要注意一点,就是根据你的业务逻辑监听依赖,不要频繁的去做事件绑定。然后就是useEffect的return函数,执行删除监听的操作。

  useRef的用法很多,这里仅介绍如何实现我们常用的获取dom,绑定事件的功能。

const scrollEle = useRef();  // 滚动条dom对象

useEffect(() => {
    const handleMouse = (v) => {};
    const handleScroll = (e) => {};
    //  初始化事件
    const init = () => {
      if(list.length > rows) {
        scrollEle.current.scrollTop = 0
        scrollEle.current.addEventListener('scroll', handleScroll);
        scrollEle.current.addEventListener('mousedown', () => handleMouse(true));
        scrollEle.current.addEventListener('mouseup', () => handleMouse(false));
      }
    }
    // 卸载前,取消监听
    const unInit = () => {
      if(list.length > rows && scrollEle.current) {
        scrollEle.current.removeEventListener('scroll', handleScroll, false);
        scrollEle.current.removeEventListener('mousedown', handleMouse, false);
        scrollEle.current.removeEventListener('mouseup', handleMouse, false);
      }
    };
    init();
    return unInit;
  }, [list, rows, rowHeight, listHeight]);
return (<div ref={scrollEle}></div>)

 

4、useEffect中用到了navigate、dispatch 或者其他库变量,导致多次执行怎么处理

  在目录的第2条我们已经聊过useEffect执行机制,及发http请求的问题了,关于用法我不赘言。那么有些情况下我们会在useEffect中用到navigate、dispatch 甚至是其他一些库文件,比如echarts等等,如果用到了,eslint提示我们需要注入依赖。这个时候又该怎么办。这里有一个感觉不算特别合适办法,用 useRef 处理。

  看下边的删减代码(源码文件 src\layouts\BasicLayout.jsx),具体场景是我请求数据,校验权限,存储redux,没权限的跳转登陆。如果useEffect直接监听navigate,那每次跳转路由,navigate都会改变,导致useEffect执行,这不是我想要的。所以我们用useRef取一下navigate,useEffect中使用这个变量。因为useRef声明的变量不会引起函数组件的变化,useEffect自然监听不到。这样可以变相的规避这个警告。包括其他场景也是,比如 a=b+c,但是你只想在b变化的时候重新计算,c只需要获取他最新的值即可,那用useRef是最合适的方式。

const navigate = useNavigate();
const navigation = useRef(navigate);
useEffect(() => {
   dispatch(setUser(param))
   navigation.current.navigate('/login');
 }, [dispatch, navigation]);

 

5、useMemo 什么时候用?怎么用?

  useMemo他是一个辅助hook,官方建议大量使用,当然不是乱用,适可而止。既然是辅助钩子,也就是说,不用,你的代码照常运行,只是可能比较费CPU,严重的系统就很卡顿。用了,会提升系统的运行速度和流畅度。

  那什么时候用?第一,不用他,代码逻辑正常,加上他,逻辑也不会变,只是减少了渲染次数; 第二,你很确定某个地方会多执行多次,而且清楚哪些变量变化才应该重新渲染。做到这两点就不会乱用useMemo了,官方也提示了,绝对不能利用useMemo的特性去实现自己的代码逻辑,有需要用其他钩子。

  看代码(文件路径  src\layouts\BasicLayout.jsx),我们先了解一下 useMemo 的执行机制,看控制台,输出 1 2 3 ,思考一下,不难发现,useMemo 的执行时机很早,函数组件第一次执行的时候,他就执行了,而且执行在 return 的前面。然后,如果监听的变量变化,他会再次执行,否则就不会在执行。

  再看代码,我有一个渲染路由的函数,他没有使用任何变量,所以我认为他这辈子执行一次就够了,不需要任何依赖再次触发更新。所以我传了空数组。当然如果你用了别的依赖,放到数组中监听就好了。

  最后,react官方建议我们大量使用,但实际上我们不必较真,比如一个增删改查界面,随便一个变量变化都会重新执行函数组件,我们没必要将所有组件都用useMemo都包裹一下。那样代码可读性也会降低很多。只是在那些组件嵌套层级比较深,比如3层、4层这种的,如果因为父组件执行会引起子组件不断执行,就需要useMemo优化一下。比如,像我渲染路由的这种情况。

// 递归路由
const mapMenu = (l) => {};
const BasicLayout = () => {
  console.log('-----1------')
  // 渲染路由,减少页面渲染次数
  const renderRoute = useMemo(() => {
    console.log('-----2---')
    mapMenu(MENU)
  }, []);
  console.log('-----3------')

return (<div >2121212</div>)
  }

 

posted @ 2021-12-30 17:22  Mr.聂  阅读(4008)  评论(5编辑  收藏  举报