需求:
点击按钮,请求成功返回后,置灰60秒倒计时,不允许点击
碰到问题:
1、页面有个loading,在loading为true时,加载超过1秒后,计数不连贯
2、代码写到一个文件中,耦合严重
3、浏览器切换后定时器停止执行,浏览器切换tab页面后,切换回去,仍有计数,并停止
解决方法
1、在loading结束后,再开始计数
2、抽离代码,模块化
3、浏览器监听事件 document.addEventListener(‘visibilitychange’,()=>{})
浏览器标签页被隐藏或显示的时候会触发visibilitychange事件。
页面代码实现
vue文件
<u-button
:disabled="store.state.timer.remainTime > 0"
:loading="triggerLoading"
type="primary"
@click="handleTriggerTaskUploadSyncReportJob"
>
<template v-if="store.state.timer.remainTime > 0">
<u-icon type="time" />
{{ store.state.timer.remainTime + ' ' }}
</template>
同步记录
</u-button>
import { useStore } from 'vuex';
import { useRouter, onBeforeRouteLeave } from 'vue-router';
import {
triggerTaskUploadSyncReportJobApi
} from '@/api/statistics';
import visibility from '@/mixins/visibility';
import { createTimer, removeTimer } from './hooks/createTimer';
mixins: [visibility],
// #region 同步记录按钮
// 同步请求,接口同步数据结束后,同步成功返回true
// 同步返回后,执行请求列表接口,查询列表
// 60秒内,禁止点击再次点击同步记录
// 页面离开时,关闭定时器
// 页面刷新后,如果存在页面剩余刷新时间,则重新启动定时器
const triggerLoading = ref(false); // 同步记录加载状态
// 退出页面时,关闭定时器
onBeforeRouteLeave(() => removeTimer());
// #endregion 同步记录
if (triggerLoading.value) {
createTimer(true); // 主动(true)创建定时器
triggerLoading.value = false;
}
// 同步记录
const handleTriggerTaskUploadSyncReportJob = () => {
triggerLoading.value = true;
triggerTaskUploadSyncReportJobApi({ uploadDate: parseTime(new Date(), 'YYYY-MM-DD') }).then(
res => {
if (res) {
getList(); // 刷新列表
}
}
);
};
onMounted(() => {
getList();
createTimer(false); // 被动(false)创建定时器
});
return {
triggerLoading, // 同步记录按钮
handleTriggerTaskUploadSyncReportJob,
store // 剩余时间
};
createTimer.js-创建定时器
// 同步记录按钮
// 同步请求,接口同步数据结束后,同步成功返回true
// 同步返回后,执行请求列表接口,查询列表
// 主动启动定时器,60秒内,禁止点击再次点击同步记录
// 页面离开时,关闭定时器
// 页面刷新后,如果存在页面剩余时间,则重新被动启动定时器
import store from '@/store';
import { setInterValCustom, cancleInterValCustom } from './timer';
// 生成剩余时间-60秒内静止点击
export const generateRemainTime = () => {
if (localStorage.getItem('syncRecordsStartTime')) {
// 存在,则获取剩余时间
return (
60 - Math.floor((Date.now() - (localStorage.getItem('syncRecordsStartTime') || 0)) / 1000)
);
}
return 0; // 不存在同步剩余时间
};
// 是否执行循环方法
const isCall = () => {
const remainTime = generateRemainTime(); // 剩余时间
// 不存在或刚结束
if (remainTime === 0) {
return true;
}
// 存在剩余时间,并且剩余时间已经改变,下一秒
return remainTime > 0 && store.state.timer.remainTime !== remainTime;
};
// 循环执行函数
const loop = timerId => {
const remainTime = generateRemainTime(); // 获取剩余刷新时间
store.dispatch('setRemainTime', remainTime); // 存储剩余时间到缓存,用于页面显示和判断
store.dispatch('setTimerId', timerId); // 存储定时器id,到缓存,用于清除定时器
// 结束循环,标志 remainTime = 0
if (remainTime <= 0) {
// eslint-disable-next-line no-use-before-define
removeTimer(); // 清理定时器
localStorage.removeItem('syncRecordsStartTime'); // 清除缓存刷新开始时间
}
};
// 创建自定义定时器
export const createTimer = isActive => {
// isActive 是否主动触发,点击按钮时,主动出发,刷新页面时,如果刷新时间未结束,是被动
if (isActive) {
localStorage.setItem('syncRecordsStartTime', Date.now());
}
setInterValCustom(loop, isCall); // 设置定时器
};
// 移除定时器
export const removeTimer = () => {
cancleInterValCustom(store.state.timer.timerId); // 清除定时器
store.dispatch('setRemainTime', 0); // 归零剩余时间
store.dispatch('setTimerId', null); // 清除定时器id
};
timer.js-定时器调用
import { getRequestAnimationFrame, cancelRequestAnimationFrame } from './raf';
// 自定义定时器
export const setInterValCustom = (fn, isCall) => {
let timer;
const raf = getRequestAnimationFrame(); // 获取定时器方法
const loop = () => {
// 循环定时器
timer = raf(loop);
// 如果条件成立,执行回调函数
if (isCall()) {
fn.call(this, timer); // 调用回调方法
}
};
timer = raf(loop); // 执行动画回调
};
// 关闭定时器
export const cancleInterValCustom = timer => {
cancelRequestAnimationFrame(timer);
};
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;
};
moudle/timer.js-存储剩余时间和定时器id
const app = {
state: {
remainTime: 0,
timerId: null
},
mutations: {
REMAIN_TIME: (state, remainTime) => {
state.remainTime = remainTime || 0;
},
TIMER_ID: (state, timerId) => {
state.timerId = timerId;
}
},
actions: {
setRemainTime({ commit }, remainTime) {
commit('REMAIN_TIME', remainTime);
},
setTimerId({ commit }, timerId) {
commit('TIMER_ID', timerId);
}
},
getters: {
remainTime: state => state.remainTime || 0,
timerId: state => state.timerId
}
};
export default app;
visibility.js-浏览器切换后定时器停止执行
import { createTimer, removeTimer } from '@/views/statistics/uploadRecord/hooks/createTimer';
// 如果在剩余时间结束时,浏览器切换到其它tab,会导致页面仍然停留剩余时间
// 改进,在浏览器tab页面切换时,切换到其它tab,关闭定时器,切换到本tab后,启动定时器
export default {
created() {
window.document.addEventListener('visibilitychange', this.visibilityChange);
},
beforeRouteLeave() {
window.document.removeEventListener('visibilitychange', this.visibilityChange);
},
methods: {
visibilityChange() {
// document 身上有一个属性叫作 visibilityState
// 表示当前页面是显示或者隐藏状态
if (document.visibilityState === 'hidden') {
// 如果隐藏(最小化,其他网页)
// 关闭定时器
removeTimer();
} else if (document.visibilityState === 'visible') {
// 开启定时器
createTimer(false);
}
}
}
};