Skip to content
On this page

React Hooks 实现倒计时及避坑

正确示例 1

jsx
import { useState, useEffect } from "react";

// 入参是一个时间段,比如6000s,不是具体的过期时间点,因为考虑到用户可能会修改电脑的时间,这样在获取当前时间的时候就会产生误差
export const CountDown = ({ countDown = 4 }) => {
  let cd = countDown;
  let timer = null;

  const [time, setTime] = useState("");

  const handleData = () => {
    if (cd <= 0) {
      setTime("到时间啦");
      return timer && clearTimeout(timer);
    }
    const d = parseInt(cd / (24 * 60 * 60) + "");
    const h = parseInt(((cd / (60 * 60)) % 24) + "");
    const m = parseInt(((cd / 60) % 60) + "");
    const s = parseInt((cd % 60) + "");
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    cd--;
    timer = setTimeout(() => {
      handleData();
    }, 1000);
  };

  useEffect(() => {
    handleData();
    return () => {
      timer && clearTimeout(timer);
    };
  }, []);

  return <div>{time}</div>;
};
export default CountDown;
import { useState, useEffect } from "react";

// 入参是一个时间段,比如6000s,不是具体的过期时间点,因为考虑到用户可能会修改电脑的时间,这样在获取当前时间的时候就会产生误差
export const CountDown = ({ countDown = 4 }) => {
  let cd = countDown;
  let timer = null;

  const [time, setTime] = useState("");

  const handleData = () => {
    if (cd <= 0) {
      setTime("到时间啦");
      return timer && clearTimeout(timer);
    }
    const d = parseInt(cd / (24 * 60 * 60) + "");
    const h = parseInt(((cd / (60 * 60)) % 24) + "");
    const m = parseInt(((cd / 60) % 60) + "");
    const s = parseInt((cd % 60) + "");
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    cd--;
    timer = setTimeout(() => {
      handleData();
    }, 1000);
  };

  useEffect(() => {
    handleData();
    return () => {
      timer && clearTimeout(timer);
    };
  }, []);

  return <div>{time}</div>;
};
export default CountDown;

正确示例 2

jsx
import { useState, useEffect, useRef } from "react";

const CountDown = ({ countDown }) => {
  const cd = useRef(countDown);
  const timer = useRef(null);

  const [time, setTime] = useState("");

  const dealData = () => {
    if (cd.current <= 0) {
      setTime("");
      return timer.current && clearTimeout(timer.current);
    }
    const d = parseInt(cd.current / (24 * 60 * 60) + "");
    const h = parseInt(((cd.current / (60 * 60)) % 24) + "");
    const m = parseInt(((cd.current / 60) % 60) + "");
    const s = parseInt((cd.current % 60) + "");
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    cd.current--;
    timer.current = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect(() => {
    dealData();
    return () => {
      timer.current && clearTimeout(timer.current);
    };
  }, []);

  return <div>{time}</div>;
};

export default CountDown;
import { useState, useEffect, useRef } from "react";

const CountDown = ({ countDown }) => {
  const cd = useRef(countDown);
  const timer = useRef(null);

  const [time, setTime] = useState("");

  const dealData = () => {
    if (cd.current <= 0) {
      setTime("");
      return timer.current && clearTimeout(timer.current);
    }
    const d = parseInt(cd.current / (24 * 60 * 60) + "");
    const h = parseInt(((cd.current / (60 * 60)) % 24) + "");
    const m = parseInt(((cd.current / 60) % 60) + "");
    const s = parseInt((cd.current % 60) + "");
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    cd.current--;
    timer.current = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect(() => {
    dealData();
    return () => {
      timer.current && clearTimeout(timer.current);
    };
  }, []);

  return <div>{time}</div>;
};

export default CountDown;

错误示例

jsx
import { useState, useEffect } from "react";

const CountDown = ({ countDown = 5 }) => {
  let [cd, setCd] = useState(countDown);
  let timer = null;

  const [time, setTime] = useState("");

  const dealData = () => {
    if (cd <= 0) {
      setTime("");
      return timer && clearTimeout(timer);
    }
    const d = parseInt(cd / (24 * 60 * 60) + "");
    const h = parseInt(((cd / (60 * 60)) % 24) + "");
    const m = parseInt(((cd / 60) % 60) + "");
    const s = parseInt((cd % 60) + "");
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    setCd(cd--); // 应该是setCd(cd - 1)
    timer = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect(() => {
    dealData();
    return () => {
      timer && clearTimeout(timer);
    };
  }, []);

  return <div>{time}</div>;
};

export default CountDown;
import { useState, useEffect } from "react";

const CountDown = ({ countDown = 5 }) => {
  let [cd, setCd] = useState(countDown);
  let timer = null;

  const [time, setTime] = useState("");

  const dealData = () => {
    if (cd <= 0) {
      setTime("");
      return timer && clearTimeout(timer);
    }
    const d = parseInt(cd / (24 * 60 * 60) + "");
    const h = parseInt(((cd / (60 * 60)) % 24) + "");
    const m = parseInt(((cd / 60) % 60) + "");
    const s = parseInt((cd % 60) + "");
    setTime(`倒计时: ${d}${h}${m}${s}秒`);
    setCd(cd--); // 应该是setCd(cd - 1)
    timer = setTimeout(() => {
      dealData();
    }, 1000);
  };

  useEffect(() => {
    dealData();
    return () => {
      timer && clearTimeout(timer);
    };
  }, []);

  return <div>{time}</div>;
};

export default CountDown;

错误示例会出现的问题

  • cd 在 useEffect 中始终是初始值;
  • 在 DOM 元素上,会出现先是初始值,然后从初始值减 1 ,之后就不会在变化

原因

  • 因为 useEffect 的第二个参数是 [] ,所以 re-render (更新渲染)的时候不会重新执行 effect 函数,所以 cd 在 useEffect 中始终是初始值;
  • 因为 useEffect 第一次执行的时候即初始化的时候利用 setCd(cd--) 重新对 cd 赋值,所以会触发视图重新渲染一次;
  • 如果 useEffect 没有第二个参数 [] ,既没有依赖,那么就相当于 didMount 和 DidUpdate 两个生命周期的合集,所以更新的时候会重新执行 useEffect 【注意】: useEffect 的第一个参数是一个匿名函数,匿名函数执行完会立即被销毁,所以在组件重新更新的时候,会重新调用匿名函数,并获取更改后的 cd 值

解决方案

如上示例 1 , 2 ,因为方式 1 打破了 React 纯函数的规则,所以更加建议方式 2

TIP

第一段代码使用了普通的变量 cd 和 timer,它们被定义在组件函数的顶层。这意味着每次组件函数被调用时,都会重新创建新的 cd 和 timer,而不是保留它们的状态。这样做违反了 React 纯函数组件的规则,因为它引入了外部的状态管理。

第二段代码使用了 useRef 来创建了 cd 和 timer 这两个变量的引用。这意味着它们会被保存在组件的生命周期之外,并且在组件的多次渲染之间保持不变。因此,即使组件函数被重新调用,这些引用的值也会保持不变。这样做遵守了 React 纯函数组件的规则,因为它不引入外部状态管理。

因此,第二段代码符合 React 纯函数组件的规则,而第一段代码打破了这些规则。

【注意】

  • 在倒计时为 0 的时候一定要销毁倒计时
  • 在组件销毁的时候一定要手动销毁倒计时,否则即使跳转到其他页面,倒计时依旧在进行