前端如何实现精准的倒计时(排除误差、时间偏差)
背景:
前端倒计时
的功能在很多业务场景都可以用到,比如活动开始、结束、秒杀倒计时等等等…我最近在处理流程审批及合同签署的项目,也涉及到倒计时功能,但在开发过程中遇到一些麻烦和坑点,下面和大家分享一下最后是如何解决的:
1、为什么使用setTimeout实现倒计时,而不是setInterval?
正常来说,定时器功能主要由setTimeout()
和setInterval()
这两个函数来完成,它们的内部运行机制完全一样,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。(区别在于前者指定的代码是一次性执行,后者则为反复执行
。)
- 那么为什么使用
setTimeout
实现倒计时,而不是setInterval
呢?
setInterval
指定的是 “开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间
。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。
为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。
var timer = setTimeout(function f() {// ...timer = setTimeout(f, 1000);
}, 1000);
上面代码可以确保,下一次执行总是在本次执行结束之后的1000毫秒开始。(当然存在较小误差,不然就没这道题了。。)
setTimeout
的作用是 将代码推迟到指定时间执行 ,如果指定时间为0setTimeout(f, 0),那么会立刻执行吗?
答案是不会。因为必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f。也就是说,setTimeout(f, 0)会在下一轮事件循环
一开始就执行。
2、setTimeout倒计时为什么会出现误差?
首先,js是单线程,同一时间只能做一件事情。如果前面一个任务执行时间很长(比如网络请求),后面就必须的等待很长时间。为了解决这个问题,js分为同步任务和异步任务。js会先执行同步任务,执行完后,才会去执行异步任务,异步任务一般放在异步队列中。也就是执行完同步任务后,会不断从异步队列中取出要执行的任务放在主栈中执行,这个过程就称为"event-loop
"。异步队列分为宏任务队列和微任务队列,微任务队列执行顺序大于宏任务队列。
所以:setTimeout是一个异步的宏任务,当执行setTimeout时是将回调函数在指定的时间之后放入到宏任务队列。但如果此时主线程有很多同步代码在等待执行,或者微任务队列以及当前宏任务队列之前还有很多任务在排队等待执行,那么要等他们执行完成之后setTimeout的回调函数才会被执行,因此 并不能保证在setTimeout中指定的时间立刻执行回调函数
所以,
setTimeout出现误差是因为
:1. 没有考虑误差时间(函数执行的时间/其它代码的阻塞)
2. 没有考虑浏览器的“休眠”
如果上面这个大家看不太懂,可以转战我的另一篇文章,先了解一下**event-loop
**
3、那么如何解决呢?
方法一:获取服务器时间到前端显示倒计时
先分析一下过程:
1. 客户端http请求服务器时间;
2. 服务器响应完成;
3. 服务器通过网络传输时间数据到客户端;
4. 客户端根据活动开始时间和服务器时间差做倒计时显示;
服务器响应完成的时间其实就是服务器时间,但经过网络传输这一步,就会产生误差了,误差大小视网络环境而异,这部分时间前端也没有什么好办法计算出来,一般是几十ms以内,大的可能有几百ms。
可以得出:当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间 + 常量(可选),这里重点是说要考虑前端渲染的时间,避免不同浏览器渲染快慢差异造成明显的时间不同步,这是第一点。(网络传输时间忽略或加个常量呗)
获得服务器时间后,前端进入倒计时计算和计时器显示,这步就要考虑 js代码冻结和线程阻塞
造成计时器延时问题了,我的思路是通过引入计数器,判断计时器延迟执行的时间来调整,尽量让误差缩小,不同浏览器不同时间段打开页面倒计时误差可控制在1s以内。
关键实现代码如下:
//继续线程占用
setInterval(function(){ var j = 0; while(j++ < 100000000);
}, 0); //倒计时
var interval = 1000,ms = 50000, //从服务器和活动开始时间计算出的时间差,这里测试用50000mscount = 0,startTime = new Date().getTime();
if( ms >= 0){var timeCounter = setTimeout(countDownStart,interval);
}function countDownStart(){count++;var offset = new Date().getTime() - (startTime + count * interval);var nextTime = interval - offset;var daytohour = 0; if (nextTime < 0) { nextTime = 0 };ms -= interval;console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");if(ms < 0){clearTimeout(timeCounter);}else{timeCounter = setTimeout(countDownStart,nextTime);}}
运行结果:
结论:由于线程阻塞延迟问题,做了setTimeout执行时间的误差修正,保证setTimeout执行时间一致。若冻结时间特别长的,还要做特殊处理。
方法二:web worker
Web Worker 是HTML5标准的一部分,允许一段JavaScript程序运行在主线程之外的另外一个线程中。因此可以考虑在主线程之外再创建一个 worker 线程用来处理花费大量时间的任务,这样主线程的 UI 渲染就不会被阻塞了。
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和channel属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序(反之亦然)。
在这里,我就以vue项目为例:vue中使用web worker 实现倒计时
1、首先,引入worker
yarn add worker-loader -D
vue.config.js文件:
// 解决打包的时报错parallel: false,chainWebpack: (config) => {// set worker-loaderconfig.module.rule('worker').test(/\.worker\.js$/).use('worker-loader').loader('worker-loader').options({inline: 'fallback',filename: 'workerName.[hash].worker.js',}).end();// 解决:worker 热更新问题config.module.rule('js').exclude.add(/\.worker\.js$/);// 删除splitChunks 请勿删除config.optimization.delete('splitChunks');config.plugin('define').tap((args) => {const [define] = args;Object.assign(define, {IS_APPLET: JSON.stringify(true),UNIQUE_MARK: JSON.stringify(uniqueMark),});return args;});},
2、一个专门管理worker交互的js文件
import Worker from './count.worker';
const worker = new Worker();
// 初始化倒计时
export function initCount() {worker.postMessage({type: 'init',data: {},});
}
// 暂停时间
export function pauseTime(data) {worker.postMessage({type: 'pause',data,});
}// 清除计时器
export function clearTimer() {worker.postMessage({type: 'clear',data: {},});
}
// 移除worker
export function removeWorker() {worker.terminate();
}
// 根据接口返回的时间校准考试剩余时间
export function alignTime(data) {worker.postMessage({type: 'align',data,});
}
// 开启倒计时
export function startCount (data) {worker.postMessage({type: 'start',data,});
}
// 获取worker
export function getWorker () {return worker;
}
3、使用 Worker,在worker线程中执行我们的倒计时
const countObj = {// 存储settimeouttimer: null,// 剩余倒计时时间examTime: -1,// 倒计时是否暂停的标识stopTimeStatus: false,setTimer(data) {this.timer = data;},setExamTime(data) {this.examTime = data;},setStopTimeStatus(data) {this.stopTimeStatus = data;},
};function countDown() {if (countObj.timer) {return;}if (countObj.timer === 0) {postMessage(['timeEnd', 0]);return;}// const start = new Date().getTime();countObj.timer = setTimeout(() => {countObj.timer = null;if (!countObj.getStopTimeStatus()) {countObj.setExamTime(countObj.getExamTime() - 1);}// const end = new Date().getTime();// console.log('误差', end - start);postMessage(['countDown', countObj.getExamTime()]);countDown();}, 1000);
}onmessage = function (e) {const {data: { type, data },} = e;switch (type) {case 'start':countObj.setExamTime(data);countDown();break;case 'align':countObj.setExamTime(data);postMessage(['alignTime', data]);break;case 'pause':countObj.setStopTimeStatus(data);break;case 'init':countObj.setTimer(null);countObj.setExamTime(-1);countObj.setStopTimeStatus(false);break;case 'clear':clearTimeout(countObj.timer);countObj.setTimer(null);break;default:break;}
};
4、vue文件中挂载倒计时
import * as countDownInstance from '../utils/countDown';mounted() {// 开始倒计时countDownInstance.startCount(this.examTime);const worker = countDownInstance.getWorker();worker.onmessage = async (event) => {const { data } = event;const [type, time] = data;if (type === 'countDown') {this.examTime = time;}if (type === 'timeEnd') {await this.autoSubmitPaper();this.$message.warning('时间已结束');this.examTime = time;}};
}
5、注意:
别忘了找个时机 把worker给终止掉,使用
worker.terminate
写的不太好 ,给大家推荐一篇文章,大家参考学习
前端如何实现精准的倒计时(排除误差、时间偏差)相关推荐
- 前端显示服务器时间,前端展示活动倒计时使用服务器时间
获取服务器时间,可以从请求头里去取,也就是这个 具体获取方法是这样的 请原谅我放荡不羁不爱打字,因为我不知道那些大神们是怎么把代码粘进来的 由于服务器获取的是英文时间,所以有八个小时的时差,也就是会比 ...
- 快速搞定前端JS面试--精准匹配大厂要求 (系列课程)
说明:本系列博客来源于慕课网@双越老师课程<前端JavaScript面试-精准匹配大厂面试要求>,此博客做了简要总结,需要看课程的可以移步学习. 第一章-概述[说说面试那些事] 第二章 J ...
- 误差、方差、偏差、噪声、训练误差+验证误差、偏差方差窘境、错误率和误差、过拟合与欠拟合
误差.方差.偏差.噪声.训练误差+验证误差.偏差方差窘境.错误率和误差.过拟合与欠拟合 目录
- Android 节操视频播放器jiecaovideoplayer自定义播放音频使用:屏蔽全屏按钮,增加倒计时,当前时间/总时间
一.屏蔽全屏按钮 找到JCVideoPlayerStandard.java文件中的代码: private void fixAudio() {if (SrcType.equalsIgnoreCase(& ...
- 前端性能指标:白屏和首屏时间的计算
作者:Hanpeng_Chen 公众号:前端极客技术 文章首发个人博客:前端性能指标:白屏和首屏时间的计算|代码视界 前言 页面性能优化是前端开发中一个重要的环节,而评判前端性能的优劣有两个比较经常听 ...
- 一文读懂误差的偏差方差
目录 偏差和方差的直观理解 误差分解 偏差和方差的平衡 为什么模型越复杂,方差越高呢? 通过外在表现判断模型欠拟合还是过拟合 偏差和方差的直观理解: 偏差:描述的是预测值的期望与真实值 ...
- 通俗理解误差、偏差、方差以及它们和过拟合、欠拟合之间的关系.
文章目录 0. 引言 1. 误差.偏差和方差的数学定义 2. 偏差与方差的直观理解 3. 偏差.方差与欠拟合.过拟合的关系 4. 欠拟合.欠拟合的产生原因及解决方案 0. 引言 作为一名算法工程师,在 ...
- 机器学习:算法中的泛化误差、偏差、方差、噪声的理解(超详细)
摘要:在现实任务中,我们往往有多种学习算法可供选择,甚至对同一个学习算法,当使用不同的参数配置时,也会产生不同的模型,那么,我们该如何选用哪一个学习算法,使用哪一种参数配置呢?这就是机器学习中的&qu ...
- web前端处理订单待支付倒计时计算显示问题
在商城类项目的时候,有很多待支付的订单,有时候在订单列表页面会分别显示倒计时,就是页面会有很多倒计时的订单. 处理方法: 1.调用后端接口拿到所有的订单,获取所有的倒计时订单,获取到期时间(尽量时间戳 ...
最新文章
- leetcode--电话号码和字母组合--python
- 【Windows 逆向】CE 地址遍历工具 ( CE 结构剖析工具 | 遍历查找后坐力数据 | 尝试修改后坐力数据 )
- 云闪付单个红包最高2018,这是要打败支付宝的节奏吗?
- 计算机网络之网络层:11、移动IP
- bat for 循环中定义变量(变量值不显示,通过使用「延期变量扩展」方式解决)
- (5)Matplotlib_grid
- 2013年12月24号感受
- 康威生命游戏-蓝桥杯java
- 基于深度学习的短时交通流预测与优化
- 玩真的了!深度解读拒不履行信息网络安全管理义务将入罪
- python之Scrapy 的Xpath常用定位相关
- 从零开始写 win32 打印机任务管理的 node 模块 (2)node-addon-api
- python微信公众号翻译功能怎么用_使用python一步一步搭建微信公众平台(二)----搭建一个中英互译的翻译工具...
- rancher2.6部署k8s集群示例
- 危化品企业双重预防机制数字化系统怎样建?
- 西门子精智HMI-TP1200发邮件功能
- 各品牌网络监控摄像头RTSP地址查询
- 计算机远程用户关机指令,详细介绍一个远程关机命令
- PHP获取每个周五或周一的日期
- 美丽乡村——大堰镇南溪村