setTimeout 的 hook,返回 clear 函数来清除定时器
const useTimeout = (fn: () => void, delay?: number) => {
// 使fn函数对象地址不变,哪怕fn内含有状态也不变
const timerCallback = useMemoizedFn(fn)
// 使setInterval的timer不变
const timerRef = useRef<NodeJS.Timer | null>(null)
// 使clear函数对象地址不变
const clear = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}, [])
useEffect(() => {
// 校验delay
if (!isNumber(delay) || delay < 0) {
return
}
// 保存最新定时器timer
timerRef.current = setTimeout(timerCallback, delay)
return clear
}, [delay])
return clear
}
用 requestAnimationFrame 模拟实现的 useTimeout,更精准,页面不渲染的时候不触发函数执行,比如页面隐藏或最小化等
interface Handle {
id: number | NodeJS.Timeout
}
// 先实现setRafTimeout
const setRafTimeout = function (
callback: () => void,
delay: number = 0,
): Handle {
// 如果是node等环境没有requestAnimationFrame,直接使用setTimeout调用后的定时器id
if (typeof requestAnimationFrame === typeof undefined) {
return {
id: setTimeout(callback, delay),
}
}
// 记录定时器id
const handle: Handle = {
id: 0,
}
// 记录开始时间
const startTime = new Date().getTime()
// 递归调用requestAnimationFrame,这是requestAnimationFrame的使用方式
const loop = () => {
// 记录当前时间,话说这里也能用参数获取requestAnimationFrame调用的时间
const current = new Date().getTime()
// 如果delay的时间到了,执行callback
if (current - startTime >= delay) {
callback()
} else {
// 否则继续递归
handle.id = requestAnimationFrame(loop)
}
}
// 记录定时器id
handle.id = requestAnimationFrame(loop)
return handle
}
// 判断是否有cancelAnimationFrame
function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {
return typeof cancelAnimationFrame === typeof undefined
}
// 清除setRafTimeout定时器
const clearRafTimeout = function (handle: Handle) {
// 如果是node等环境没有cancelAnimationFrame使用clearTimeout
if (cancelAnimationFrameIsNotDefined(handle.id)) {
return clearTimeout(handle.id)
}
cancelAnimationFrame(handle.id)
}
// 和useTimeout逻辑类似
function useRafTimeout(fn: () => void, delay: number | undefined) {
const fnRef = useLatest(fn)
const timerRef = useRef<Handle>()
useEffect(() => {
if (!isNumber(delay) || delay < 0) return
timerRef.current = setRafTimeout(() => {
fnRef.current()
}, delay)
return () => {
if (timerRef.current) {
clearRafTimeout(timerRef.current)
}
}
}, [delay])
const clear = useCallback(() => {
if (timerRef.current) {
clearRafTimeout(timerRef.current)
}
}, [])
return clear
}
setInterval 的 hook,返回 clear 函数来清除定时器
const useInterval = (
fn: () => void,
delay?: number,
options: { immediate?: boolean } = {},
) => {
// 使fn函数对象地址不变,哪怕fn内含有状态也不变
const timerCallback = useMemoizedFn(fn)
// 使setInterval的timer不变
const timerRef = useRef<NodeJS.Timer | null>(null)
// 使clear函数对象地址不变
const clear = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current)
}
}, [])
useEffect(() => {
// 校验delay
if (!isNumber(delay) || delay < 0) {
return
}
// 如果配置immediate则第一次渲染也执行fn
if (options.immediate) {
timerCallback()
}
// 保存最新定时器timer
timerRef.current = setInterval(timerCallback, delay)
return clear
}, [delay, options.immediate])
return clear
}
用 requestAnimationFrame 模拟实现的 useInterval,更精准,页面不渲染的时候不触发函数执行,比如页面隐藏或最小化等
interface Handle {
id: number | NodeJS.Timer
}
// 和useRafTimeout中类似
const setRafInterval = function (
callback: () => void,
delay: number = 0,
): Handle {
if (typeof requestAnimationFrame === typeof undefined) {
return {
id: setInterval(callback, delay),
}
}
let start = new Date().getTime()
const handle: Handle = {
id: 0,
}
const loop = () => {
const current = new Date().getTime()
if (current - start >= delay) {
callback()
start = new Date().getTime()
}
handle.id = requestAnimationFrame(loop)
}
handle.id = requestAnimationFrame(loop)
return handle
}
function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {
return typeof cancelAnimationFrame === typeof undefined
}
const clearRafInterval = function (handle: Handle) {
if (cancelAnimationFrameIsNotDefined(handle.id)) {
return clearInterval(handle.id)
}
cancelAnimationFrame(handle.id)
}
// 和useInterval中类似
function useRafInterval(
fn: () => void,
delay: number | undefined,
options?: {
immediate?: boolean
},
) {
const immediate = options?.immediate
const fnRef = useLatest(fn)
const timerRef = useRef<Handle>()
useEffect(() => {
if (!isNumber(delay) || delay < 0) return
if (immediate) {
fnRef.current()
}
timerRef.current = setRafInterval(() => {
fnRef.current()
}, delay)
return () => {
if (timerRef.current) {
clearRafInterval(timerRef.current)
}
}
}, [delay])
const clear = useCallback(() => {
if (timerRef.current) {
clearRafInterval(timerRef.current)
}
}, [])
return clear
}
倒计时 Hook
export type TDate = dayjs.ConfigType
export interface Options {
leftTime?: number
targetDate?: TDate
interval?: number
onEnd?: () => void
}
export interface FormattedRes {
days: number
hours: number
minutes: number
seconds: number
milliseconds: number
}
// 计算剩余时间
const calcLeft = (target?: TDate) => {
if (!target) {
return 0
}
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
const left = dayjs(target).valueOf() - Date.now()
return left < 0 ? 0 : left
}
// 格式化毫秒为日期
const parseMs = (milliseconds: number): FormattedRes => {
return {
days: Math.floor(milliseconds / 86400000),
hours: Math.floor(milliseconds / 3600000) % 24,
minutes: Math.floor(milliseconds / 60000) % 60,
seconds: Math.floor(milliseconds / 1000) % 60,
milliseconds: Math.floor(milliseconds) % 1000,
}
}
const useCountdown = (options: Options = {}) => {
const { leftTime, targetDate, interval = 1000, onEnd } = options || {}
// 计算目标时间
const target = useMemo<TDate>(() => {
// 如果leftTime传入了值,注意undefined也算
if ('leftTime' in options) {
// 如果leftTime是>0的数字
return isNumber(leftTime) && leftTime > 0
? Date.now() + leftTime
: undefined
} else {
return targetDate
}
}, [leftTime, targetDate])
// 倒计时状态,就是countdown
const [timeLeft, setTimeLeft] = useState(() => calcLeft(target))
// 保存onEnd钩子
const onEndRef = useLatest(onEnd)
useEffect(() => {
// 没有目标时间则停止,并归零倒计时
if (!target) {
// for stop
setTimeLeft(0)
return
}
// 立即计算并保存一次
setTimeLeft(calcLeft(target))
const timer = setInterval(() => {
// 每隔interval计算并保存倒计时
const targetLeft = calcLeft(target)
setTimeLeft(targetLeft)
// 倒计时结束时清除计时器并执行onEnd钩子
if (targetLeft === 0) {
clearInterval(timer)
onEndRef.current?.()
}
}, interval)
return () => clearInterval(timer)
}, [target, interval])
// 将倒计时格式化为日期
const formattedRes = useMemo(() => parseMs(timeLeft), [timeLeft])
return [timeLeft, formattedRes] as const
}