React中useCallback和useRef的定时器实战

4,004 阅读3分钟

​ 最近写了一个(古老的)轮播组件,用的React的hooks写法,大概涉及到了我们最常用的useState, useEffect这两个hooks,还有可能(有些人)比较陌生的useRef, useCallback

​ 本篇文章的重点不是去教大家怎么实现一个轮播组件,而是想通过实战,结合业务需求让大家理解useRef, useCallback这两个hook,知道它们大概的用途,能够遇到具体需求的时候想起使用它们。

轮播

​ 轮播组件嘛,大家都懂得,需求大概分成两块,第一需要自动轮播;第二要有点击功能,点击哪个,播放哪个,并且要重置轮播时间。

首先说第一点需求,就是设置一个定时器,不断的累加、计数,如果超出了数组长度,则把计数器重置,从头开始继续轮播;第二点呢,就是需要增加一个点击事件,点击的时候要去重新设置定时器,然后从当前点击的地方,继续轮播。

所以设置定时器这个方法有两个地方都需要用到,肯定是要提取成一个function的。

在实战代码里面提取出来的这个function,就叫start

useRef

useRef 用到的最多的场景应该就是加载第三方资源了,比如引入echarts或者高德地图,初始化的时候需要传入一个dom节点,这种时候通常就用useRef来保存节点。但是在今天这个实战里,用到useRef不是来保存节点的,而是用来保存setInterval定时器返回的ID的。

从我个人的使用感受来看,我觉得函数式组件里的useRef其实有点类似于类组件里的通过this.xxx的方式来跨生命周期保存变量、数据,然后这个变量的改变也不会触发重新渲染。

基本的使用方法如下(下面这个例子只是单纯的举例,没有实际意义😄)

import React, { useRef, useEffect } from 'react';
function A(){
	const timer = useRef();
	useEffect(() =>{
		timer.current = setInterval(()=>{
		 // XXXX
		})
		return () => clearInterval(timer.current)
	},[])
	return (
		<div />
	)
}

useCallback

我们在一个函数式组件A里面定义一个方法b,当A重新渲染(执行)的时候,里面的方法b其实也会被重新生成,也就是其引用会指向一个新的地址。

import React, { useEffect } from 'react';
function A(){
	function b(){
		return '这是b方法';
	}
	useEffect(()=>{
		fetch('XXXXXX' + b());
	}, [b])   // 传入这种依赖项,毫无意义,不起作用!
	return (
		<div>{b()}</div>
	)
}

如果我们此时把函数b当作一个依赖项,传递给useEffect的第二个参数,其实是起不到我们想要的作用的,因为每次组件A重新渲染,这个useEffect的里面的第一个方法都会被重新执行一次。

这种时候useCallback就派上用场了,我们把buseCallback包裹一下,传入b所依赖的参数,如下

const bUseCallback = useCallback(b, []);
	useEffect(()=>{
		fetch('XXXXXX' + bUseCallback());
	}, [bUseCallback])   // 传入这种依赖项,才是正确的做法!

代码

对上面两个hooks做出了一点解释之后,我就直接上代码了

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

function Carousel(props) {
  const { data = [], time } = props;
  const [active, setActive] = useState(0);
  const timer = useRef(); // 保存setInterval的ID

  // 开始轮播
  const start = useCallback(() => {
    if (timer.current) {
      clearInterval(timer.current);
    }
    const len = data.length;
    timer.current = setInterval(() => {
      setActive((v) => {
        if (v >= len - 1) {
          return 0;
        }
        return v + 1;
      });
    }, time);
  }, [data, time]);


  useEffect(() => {
    start();
    return () => clearInterval(timer.current);
  }, [start]);


  function handleClick(i) {
    setActive(i);
    start();
  }

  return (
    <div>
      {
        data.map((item, index) => (
          <div
            key={item}
            style={{ color: index === active ? 'red' : 'green' }}
            onClick={() => handleClick(index)}
          >
            {item}
          </div>
        ))
      }
    </div>
  );
}

Carousel.defaultProps = {
  time: 5000, // 轮播时间 ms
  data: ['第一个', '第二个', '第三个'],
};

export default Carousel;

总结

上面代码逻辑不复杂,主要有如下几个步骤

  1. 通过useState设置一个状态,用来保存当前的激活项。有一点需要注意的是,调用setXXX更新状态的时候,最好用函数式更新,以便每次都能拿到最新状态。
  2. 提取出了start方法,用来在需要的时候启用
  3. useRef来保存定时器返回的ID,以便调用clearInterval清空
  4. 由于start方法是useEffect的依赖项,所以需要用useEffect包裹一下

以上,如有疑问或错误,欢迎在评论区交流👏👏👏