setTimeout / setInterval与usePollingRequest

124 阅读2分钟
let timerId;

function task() {
  console.log("doing task...");
  timerId = setTimeout(task, 1000);
}

function start() {
  if (!timerId) task();
}

function stop() {
  clearTimeout(timerId);
  timerId = null;
}

start(); // 开始
stop(); // 随时停止

五分钟刷新请求

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

type UsePollingRequestOptions<T> = {
  request: () => Promise<T>,
  interval?: number, // 默认5分钟
  immediate?: boolean, // 是否立即触发一次
};

export function usePollingRequest<T = unknown>({
  request,
  interval = 5 * 60 * 1000, // 默认5分钟
  immediate = true,
}: UsePollingRequestOptions<T>) {
  const [data, setData] = (useState < T) | (null > null);
  const [loading, setLoading] = useState(false);
  const timerRef = (useRef < number) | (null > null);
  const isMounted = useRef(true);

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const result = await request();
      if (isMounted.current) {
        setData(result);
      }
    } catch (error) {
      console.error("Polling request error:", error);
    } finally {
      if (isMounted.current) {
        setLoading(false);
        timerRef.current = window.setTimeout(fetchData, interval);
      }
    }
  }, [request, interval]);

  const stop = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }
  };

  const start = () => {
    stop();
    fetchData();
  };

  useEffect(() => {
    if (immediate) {
      fetchData();
    } else {
      timerRef.current = window.setTimeout(fetchData, interval);
    }

    return () => {
      isMounted.current = false;
      stop();
    };
  }, [fetchData, immediate, interval]);

  return {
    data,
    loading,
    refresh: fetchData,
    stop,
    start,
  };
}

使用案例

const fetchData = () => axios.get('/api/data').then(res => res.data);

function MyComponent() {
  const { data, loading, refresh, stop, start } = usePollingRequest({
    request: fetchData,
    interval: 5 * 60 * 1000, // 5分钟
    immediate: true,
  });

  return (
    <div>
      {loading && <p>加载中...</p>}
      <pre>{JSON.stringify(data, null, 2)}</pre>
      <button onClick={refresh}>手动刷新</button>
      <button onClick={stop}>停止</button>
      <button onClick={start}>重新开始</button>
    </div>
  );
}

JavaScript 定时器的精度受 浏览器调度主线程阻塞 影响。

这意味着 两次请求之间的间隔是 5 分钟 + 3 秒,会随着网络波动慢慢偏移。

### 使用时间差纠正偏移量:

const nextTick = () => {
  const now = Date.now();
  const offset = now % interval;
  const delay = interval - offset; // 距离下一个5分钟的毫秒数

  setTimeout(() => {
    fetchData();
    nextTick(); // 再次安排
  }, delay);

改进后的

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

type UsePollingRequestOptions<T> = {
  request: () => Promise<T>;
  interval?: number; // 间隔时间(ms),默认 5分钟
  immediate?: boolean; // 是否立即执行一次
  alignToInterval?: boolean; // 是否精确对齐周期
};

export function usePollingRequest<T = unknown>({
  request,
  interval = 5 * 60 * 1000,
  immediate = true,
  alignToInterval = true,
}: UsePollingRequestOptions<T>) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const timerRef = useRef<number | null>(null);
  const isMounted = useRef(true);

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const result = await request();
      if (isMounted.current) {
        setData(result);
      }
    } catch (err) {
      console.error('Polling error:', err);
    } finally {
      setLoading(false);
    }
  }, [request]);

  const scheduleNext = useCallback(() => {
    const now = Date.now();

    const delay = alignToInterval
      ? interval - (now % interval) // 对齐到周期
      : interval;

    timerRef.current = window.setTimeout(async () => {
      await fetchData();
      scheduleNext(); // 递归调用
    }, delay);
  }, [fetchData, interval, alignToInterval]);

  const start = useCallback(() => {
    stop();
    if (immediate) {
      fetchData().then(scheduleNext);
    } else {
      scheduleNext();
    }
  }, [fetchData, scheduleNext, immediate]);

  const stop = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }
  };

  useEffect(() => {
    isMounted.current = true;
    start();
    return () => {
      isMounted.current = false;
      stop();
    };
  }, [start]);

  return {
    data,
    loading,
    refresh: fetchData,
    start,
    stop,
  };
}