(第二版)setInterval定时器/计数器封装(settimeout\requestAnimationFrame),代码抽离,浏览器切换后定时器停止执行

548 阅读3分钟

第一版实现

第二版实现

image.png

image.png

image.png

需求:

点击按钮,请求成功返回后,置灰60秒倒计时,不允许点击

与第一版相比,优化点
  1. 系统可以多处使用,通过自定义storagekey实现
  2. 简化实现逻辑,简单易读
  3. 解绑vue-store、mixins
  4. 使用hooks
代码实现
vue文件
<u-button
  :disabled="remainTime > 0"
  :loading="timerLoading"
  type="primary"
  @click="handleTriggerTaskUploadSyncReportJob"
>
  <template v-if="remainTime > 0">
    <u-icon type="time" />
    {{ remainTime + ' ' }}
  </template>
  同步记录
</u-button>
import { triggerTaskUploadSyncReportJobApi } from '@/api/statistics';
import useTimer from '@/hooks/useTimer';
const {
  remainTime, timerLoading, createTimer
} = useTimer({
  timerName: 'transition-record',
});

// 同步记录
const handleTriggerTaskUploadSyncReportJob = () => {
  timerLoading.value = true;
  triggerTaskUploadSyncReportJobApi({ uploadDate: parseTime(new Date(), 'YYYY-MM-DD') }).then(
    res => {
      if (res) {
        getList(); // 刷新列表
      }
    }
  );
};
// 查询接口返回后执行
if (timerLoading.value) {
  createTimer(true); // 主动(true)创建定时器
  timerLoading.value = false;
}
return {
    remainTime, // 剩余时间
    timerLoading, // 同步记录按钮
}
useTimer.js
import { message } from '';
import { ref, onMounted } from 'vue';
import { onBeforeRouteLeave } from 'vue-router';
import { getRequestAnimationFrame, cancelRequestAnimationFrame } from './timer/raf';

// timerName 计时器名称,区分计时器的唯一标志
// totalSec 计时器总数,默认60s
export default function useTimer({ timerName, totalSec = 60 }) {
  if (!timerName) return message.error('计时器名称不可用为空!');

  // localstorage存储同步记录开始时间(点击时间)的key
  const storageTimerName = `__marketing-html-timer-${timerName}__`;

  // 变量
  const timerLoading = ref(false); // 同步记录按钮加载状态
  const remainTime = ref(0); // 剩余时间
  const timerId = ref(undefined); // 计时器id

  // 移除定时器
  const removeTimer = () => {
    cancelRequestAnimationFrame(timerId.value); // 关闭定时器
    timerId.value = undefined; // 计时器id
    remainTime.value = 0; // 归零剩余时间
  };

  // 生成剩余时间-totalSec秒内禁止点击
  const generateRemainTime = () => {
    if (localStorage.getItem(storageTimerName)) {
      // 存在,则获取剩余时间
      return (
        totalSec - Math.floor((Date.now() - (localStorage.getItem(storageTimerName) || 0)) / 1000)
      );
    }
    return 0; // 不存在剩余时间
  };

  // 是否执行循环方法
  const checkRemainTimeFn = () => {
    const genRemainTime = generateRemainTime(); // 剩余时间
    // 不存在或刚结束 genRemainTime = 0
    // 跳转到其他页面,或切换浏览器后,剩余时间已结束 genRemainTime < 0
    if (genRemainTime <= 0) {
      removeTimer(); // 清理定时器
      localStorage.removeItem(storageTimerName); // 清除缓存刷新开始时间
    }
    // 时间改变后,同步修改剩余时间
    if (remainTime.value !== genRemainTime) remainTime.value = genRemainTime;
  };

  // 自定义定时器
  const setInterValCustom = checkRemainTimeFn => {
    const raf = getRequestAnimationFrame(); // 获取定时器方法
    const loop = () => {
      // 循环定时器
      timerId.value = raf(loop);
      checkRemainTimeFn(); // 循环方法
    };
    timerId.value = raf(loop); // 执行动画回调
  };

  // 创建定时器
  const createTimer = isActive => {
    // isActive 是否主动触发,点击同步按钮时,主动出发,刷新页面时,如果刷新时间未结束,是被动触发
    if (isActive) {
      localStorage.setItem(storageTimerName, Date.now());
    }
    setInterValCustom(checkRemainTimeFn); // 设置定时器
  };

  // visibilitychange 事件函数
  const visibilityChange = () => {
    // document 身上有一个属性叫作 visibilityState
    // 表示当前页面是显示或者隐藏状态
    if (document.visibilityState === 'hidden') {
      // 如果隐藏(最小化,其他网页)
      // 关闭定时器
      removeTimer();
    } else if (document.visibilityState === 'visible') {
      // 开启定时器
      createTimer(false);
    }
  };

  // 1、被动创建定时器(刷新页面后,有缓存定时器,则重新激活定时器)
  // 2、监听浏览器tab切换操作
  onMounted(() => {
    createTimer(false); // 被动(false)创建定时器
    window.document.addEventListener('visibilitychange', visibilityChange);
  });

  // 退出页面时,关闭定时器 解除绑定事件
  onBeforeRouteLeave(() => {
    removeTimer();
    window.document.removeEventListener('visibilitychange', visibilityChange);
  });

  return {
    storageTimerName, // 存储key值
    remainTime, // 计时器剩余时间
    timerLoading, // 按钮加载中状态
    createTimer, // 启动定时器
    removeTimer // 关闭定时器
  };
}

raf.js
function requestAnimationFramePolyfill() {
  let lastTime = 0;
  return callback => {
    const currTime = new Date().getTime();
    const timeToCall = Math.max(0, 16 - (currTime - lastTime));
    const id = window.setTimeout(() => {
      callback(currTime + timeToCall);
    }, timeToCall);
    lastTime = currTime + timeToCall;
    return id;
  };
}

export const getRequestAnimationFrame = () => {
  if (window.requestAnimationFrame) {
    return window.requestAnimationFrame;
  }
  return requestAnimationFramePolyfill();
};

export const cancelRequestAnimationFrame = id => {
  if (window.cancelAnimationFrame) {
    return window.cancelAnimationFrame(id);
  }
  return clearTimeout(id);
};

const raf = getRequestAnimationFrame();

export const cancelAnimationTimeout = frame => cancelRequestAnimationFrame(frame.id);

export const requestAnimationTimeout = (callback, delay) => {
  const start = Date.now();
  function timeout() {
    if (Date.now() - start >= delay) {
      callback();
    } else {
      // eslint-disable-next-line no-use-before-define
      frame.id = raf(timeout);
    }
  }
  const frame = {
    id: raf(timeout)
  };
  return frame;
};