前端开发中一个不可避免的场景就是写倒计时,没有接触这个场景以前笔者一直觉得这玩意只要 setInterval 一下就能够了,最多就是有 Event Loop 致使微小偏差的坑,直到去年年初的时候写登录注册遇到了才发觉这里仍是有很多讲究的,这边文章主要是记录笔者是如何解决倒计时这种场景的实际开发中遇到的几个问题。本文会带来俩个部分同窗陌生的概念:Event Loop 和 requestAnimationFrame 涉及到篇幅问题会以后单独写文章,可是请相信笔者,即便你不懂这俩东西看完以后再去查资料也不晚。前端
结尾会附上源代码和一个基于 react hooks 的实现。react
本文涉及的源码地址:https://github.com/MonchiLin/...git
目前已经实现的功能以下:github
基于该库实现的 react hooks 版本:https://github.com/MonchiLin/...typescript
你们都知道 JS 的并发模型是 Event Loop,用起来很简单方便,可是在倒计时这种场景中会致使定时器(setInterval/setTimeout)没法精确的计算时间,正常状况下这点程度也不须要特别重视,致使时间错误的另外一个问题是在一些浏览器中( Chrome/Edge )标签页在后台时定时器是不会被执行的,这就会形成一个很大的问题,看下面的例子:npm
小红使用手机号登陆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!想必聪明的你读完这篇文章后本身造一个倒计时的轮子也不在话下,或者由后端的小伙伴配合一下,例如返回 “验证码频繁”。
另外,若是有新的功能需求也能够告诉我。