React 自定义 Hooks 之 useSetInterval

概述

在业务场景中,我们总会遇到倒计时。例如发送验证码之后的 60s 重新发送的倒计时。最近在使用 React Hook API 的时候,认为可以自定义一个 Hook,实现倒计时,于是就有了 useSetInterval 自定义 Hook

useSetInterval

这里是 demo。可以直接复制下面代码,之后在需要的页面进行引入

useSetInterval

import { useEffect, useRef } from "react";

function useSetInterval(callback, delay) {
  if (!(callback instanceof Function)) {
    throw new Error("callback 参数必须是函数!");
  }
  if (!(delay === null || typeof delay === "number")) {
    throw new Error("delay 必须是 null 或者数字!");
  }
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (delay === null) {
      return;
    }
    let id = null;
    const tick = () => {
      const returnValue = savedCallback.current();
      if (returnValue) {
        console.log("come in");
        if (returnValue instanceof Function) {
          returnValue();
        } else {
          throw new Error("返回值必须是函数!");
        }
        clearTimeout(id);
        return;
      }
      id = setTimeout(tick, delay);
    };
    id = setTimeout(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

export default useSetInterval;

使用如下

  useSetInterval(() => {
    if (count <= 0) {
      return () => {
        // 定时器消除后的代码逻辑
        setDelay(null);
        setCount(6);
      };
    }
    setCount(count - 1);
  }, delay);

React Hooks 实现倒计时

  useEffect(() => {
    let timerId = null;
    const run = () => {
      console.log("count -> ", count);
      if (count <= 0) {
        return () => {
          timerId && clearTimeout(timerId);
        };
      }
      setCount(count - 1);
      timerId = setTimeout(run, 1000);
    };
    timerId = setTimeout(run, 1000);
    return () => {
      timerId && clearTimeout(timerId);
    };
  }, [count]);

使用 React Hooks 实现倒计时是很容易的,这里是demo。这里我们需要注意一点,这里的 effect 是必须要清除的,这样才能保证我们一直只有一个定时器。另外请注意 React 何时清除 effect,React 会在组件卸载的时候执行清除操作,并且 effect 在每次渲染的时候都会执行,即 React 会在执行当前 effect 之前对上一个 effect 进行清除,当然可以传递第二个可选参数数组通知 React 跳过对 effect 的调用,从而进行性能优化

注:这里使用了 setTimeout 模拟 setInterval为啥使用 setTimeout 的原因

点击触发倒计时

  const sendCaptcha = () => {
    setSendingCaptcha(true);
    const run = () => {
      console.log("count -> ", count);
      if (count <= 0) {
        console.log("stop");
        setSendingCaptcha(false);
      }
      setCount(count - 1);
      setTimeout(run, 1000);
    };
    setTimeout(run, 1000);
  };

这里是demo,由于没有使用 useEffect,从而使得 sendCaptcha 方法拿不到最新的 count,所以 count 一直是5

解决上述问题

  useEffect(() => {
    let timerId = null;
    if (!sendingCaptcha) {
      return () => {
        timerId && clearTimeout(timerId);
      };
    }
    const run = () => {
      if (count <= 0) {
        setSendingCaptcha(false);
        return;
      }
      setCount(count - 1);
      timerId = setTimeout(run, 1000);
    };
    timerId = setTimeout(run, 1000);
    return () => {
      timerId && clearTimeout(timerId);
    };
  }, [sendingCaptcha, count]);

这里是demo,这里将 sendingCaptchacount 作为 useEffect 的依赖,从而实现了点击之后去触发倒计时

抽离 useSetInterval

设计 API

其中只需要开发者去关注自己的逻辑,而不用去关心副作用。
使用和 setInterval 相同,但是如果要停止必须要返回一个函数,函数内可以写开发者自己的逻辑,例如初始化 count,并且还要传递一个 interval 时间间隔

  useSetInterval(() => {
    if(count <= 0) {
      return () => {
        // 这个函数可以写一些定时器消除后的代码逻辑
        setCount(6);
      }
    }
    setCount(count - 1);
  }, delay);

1. 实现倒计时

function useSetInterval(callback, delay) {
  useEffect(() => {
    let timerId = null;
    const run = () => {
      const returnValue = callback();
      if (returnValue) {
        if (returnValue instanceof Function) {
          returnValue();
        } else {
          throw new Error("返回值必须是函数!");
        }
        timerId && clearTimeout(timerId);
        return;
      }
    };

    timerId = setTimeout(run, delay);
    return () => {
      timerId && clearTimeout(timerId);
    };
  }, [callback, delay]);
}

这里是demo。打开控制台可以发现 count 一直在循环,原因在于 使用 useSetInterval 时每次给到的 callback 都是新的。这时我们可以来个容器,这个容器指向 callback

2. 使用 useRef 保存 callback

  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  
  useEffect(() => {
    let timerId = null;
    const run = () => {
      const returnValue = savedCallback.current();
      if (returnValue) {
        if (returnValue instanceof Function) {
          returnValue();
        } else {
          throw new Error("返回值必须是函数!");
        }
        timerId && clearTimeout(timerId);
        return;
      }
      timerId = setTimeout(run, delay);
    };

    timerId = setTimeout(run, delay);
    return () => {
      timerId && clearTimeout(timerId);
    };
  }, [delay]);

这里是demouseRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue),返回的 ref 对象在组件的整个生命周期内保持不变。我们在 Hooks FAQ可以看到更多关于 useRef 的理解。即「ref」对象是一个 current 属性可变且可以容纳任意值的通用容器。我们还需要在 run 函数中加入

timerId = setTimeout(run, delay);

从而模拟 setInterval

3. 优化

我们要做到适时触发,即点击之后再开启定时器。这里是demo

我们通过使用参数 delay 是否为 null 来判断是否开启定时器。

useEffect(() => {
+    if (delay === null) {
+      return;
+    }
    let timerId = null;
    const run = () => {
      const returnValue = savedCallback.current();
      if (returnValue) {
        if (returnValue instanceof Function) {
          returnValue();
        } else {
          throw new Error("返回值必须是函数!");
        }
        timerId && clearTimeout(timerId);
        return;
      }
      timerId = setTimeout(run, delay);
    };

    timerId = setTimeout(run, delay);
    return () => {
      timerId && clearTimeout(timerId);
    };
  }, [delay]);

之后我们还可以设置 delay 从而控制定时器的 interval 间隔
最后再做一些类型检测即可

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容