概述
在业务场景中,我们总会遇到倒计时。例如发送验证码之后的 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,这里将 sendingCaptcha 和 count 作为 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]);
这里是demo。useRef 返回一个可变的 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 间隔
最后再做一些类型检测即可








网友评论