- [时间]: 2020/3/16
- [keyword]: 前端,npm,开发,Typescript,倒计时
前端开发中一个不可避免的场景就是写倒计时,没有接触这个场景之前笔者一直以为这玩意只要 setInterval 一下就可以了,最多就是有 Event Loop 导致微小误差的坑,直到去年年初的时候写登陆注册遇到了才发觉这里还是有不少讲究的,这边文章主要是记录笔者是如何解决倒计时这种场景的实际开发中遇到的几个问题。本文会带来俩个部分同学陌生的概念:Event Loop 和 requestAnimationFrame 涉及到篇幅问题会之后单独写文章,但是请相信笔者,即使你不懂这俩东西看完之后再去查资料也不晚。
结尾会附上源代码和一个基于 react hooks 的实现。
本文涉及的源码地址:https://github.com/MonchiLin/...
目前已经实现的功能如下:
- [x] 灵活的参数配置
- [x] 基于 raf 的定时器实现
- [x] 暂停倒计时/恢复倒计时
基于该库实现的 react hooks 版本:https://github.com/MonchiLin/...
定时器之殇
大家都知道 JS 的并发模型是 Event Loop,用起来很简单方便,但是在倒计时这种场景中会导致定时器(setInterval/setTimeout)无法精确的计算时间,正常情况下这点程度也不需要特别重视,导致时间错误的另一个问题是在一些浏览器中( Chrome/Edge )标签页在后台时定时器是不会被执行的,这就会造成一个很大的问题,看下面的例子:
小红使用手机号登录xx网站,发送验证码之后小红发现手机号填错了,于是在等待再次发送验证码的时间内她顺手打开了别的网站看了一会,这时回来发现倒计时仍然在继续。
下面代码为如何矫正倒计时:
/**
* 矫正时间
*/
rectifyTime() {
// 注: this.infoForRectification.startTime 为本次倒计时的开始的时间点
// 注: this.infoForRectification.endTime 为本次倒计时的结束时时间点
// this.infoForRectification.endTime = 本次倒计时的开始的时间点 + 需要倒计时的时间
// 这里楼主的代码已经
// 倒计时开始后经过了多久 = 当前时间 - 倒计时开始的时间
const now = new Date().getTime() - this.infoForRectification.startTime
// 完成倒计时总需时间 = 倒计时的结束时时间点 - 倒计时开始的时间点
const total = this.infoForRectification.endTime - this.infoForRectification.startTime
// 期望的当前剩余时间 = 倒计时开始后经过了多久 - 完成倒计时总需时间 (step 先无视)
const timeOfAnticipation = this.countdownConfig.step * (total - now)
console.log("期望的当前剩余时间 =>", timeOfAnticipation / 1000, "s")
console.log("实际当前剩余时间 =>", this.currentTime, "s")
// 偏差 = 当前的倒计时 - 期望的当前剩余时间 / 1000 (因为期望的剩余时间是时间戳)
const offset = this.currentTime - timeOfAnticipation / 1000
console.log("误差 =>", offset, "s")
// 处理离开屏幕太久的情况, 早就已经完成了倒计时,在调用函数的地方进行处理,若是返回 false 则认为已经倒计时结束
if (offset > this.currentTime) {
return false
} else if (offset >= this.config.precision / 1000) {
// this.config.precision:精度
// 如果误差已经大于容许的偏差则矫正一次当前的倒计时
this.currentTime -= offset
}
return true
}
更好的定时器
解决了大问题之后我们不妨更进一步来优化一下 Event Loop 导致的时间偏差, requestAnimationFrame 可以实现更加优秀的倒计时解决方案,它最大的特点就是浏览器会保证在每次刷新的屏幕的时候调用一次,那么浏览器多久刷新一次屏幕呢?取决你屏幕的刷新率,一般在 60 Hz 以上,假设浏览器一秒刷新 60 次,那么每次调用的间隔就是 1000 / 60 = 0.16 ms 这意味着我们使用 RAF(requestAnimationFrame)可以减少更多的误差,下面附上 RAF 实现 setInterval 的代码:
this.requestInterval = (fn, delay) => {
// 记录开始时间
let start = new Date().getTime()
// 创建一个对象保存 raf 的 timer 用于清除 raf
const handle: Handle = {
timer: null
};
// 创建一个闭包函数
const loop = () => {
// 每次储存 timer, 注意看,这里递归调用了 loop,这就是 raf 的用法
handle.timer = requestAnimationFrame(loop);
// loop 本次被调用的时间
const current = new Date().getTime()
// 计算距离上次调用 loop 过了多久 = 本次调用时间 - 起始时间
const delta = current - start;
// 如果 delta >= delay 就意味着已经经过 delay 的时间,将再次调用 fn
if (delta >= delay) {
fn.call();
// 重新记录开始时间
start = new Date().getTime();
}
}
handle.timer = requestAnimationFrame(loop);
return handle;
}
解决实际问题
若是要解决实际问题就必定绕不开需要偏离思路的脏代码,这部分代码并没有什么特色,主要就是为了处理边界行为,笔者再三考虑后还是觉得移除这部分文章,如果有小伙伴需要我讲解可以在告诉我,最好的方式当然还是直接看源代码,对于一些的小伙伴来说看源码要比看别人讲解快太多了。
剩下想说的
什么,你说你还要处理刷新网页后从本地读取这种情况,Oh no!想必聪明的你读完这篇文章后自己造一个倒计时的轮子也不在话下,或者由后端的小伙伴配合一下,例如返回 “验证码频繁”。
另外,如果有新的功能需求也可以告诉我。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。