1.场景
试想,现在有这样一个场景,用户有一个观看视频的任务,任务的总时间为3min,观看达到这个时间用户即可完成此任务,我们需要做一个完成倒计时,来提示用户还剩多长时间即可完成该任务。
最容易想到的实现方式是使用定时器setInterval,每隔1s倒计时减1,用户暂停播放时清除定时器,开始播放时重启定时器
let countdown = 3 * 60 let timer = null
const stop = () => { clearInterval(timer) timer = null }
const start = () => { if (!timer) { timer = setInterval(() => { if (countdown > 0) { countdown-- console.log('countdown: ', countdown) } else { console.log('done') stop() } }, 1000) } }
|
这样的实现方式在一般情况下不会出现大的问题,但是如果用户暂停次数比较多的话,会发现用户实际播放时长大于倒计时所记的时长。
想个比较及端的例子,用户每次播放时长不超过1s,那么我们的倒计时一直保持不变,显然我们实现的倒计时是不准确的,不可靠的。
可能你会想着缩短setInterval的间隔来减少这种误差,但是间隔过小,又有阻塞的可能,定时器本身也不是那么精确,总有几毫秒的误差,而且显然这种做法略显笨拙,
这时我们就要思考是不是需要换一种实现方式,更优雅一些。
要想每次暂停播放不损失那不到1s的时间,我们只有精确到ms才行,既然不能用setInterval间隔1ms,那我们是可以取当前时间的毫秒数的,这个是非常精确的,本身就是以ms为单位的
let countdown = 3 * 60 let time = 0 let startTime = 0 let timer = null
const stop = () => { time += (Date.now() - startTime) clearInterval(timer) timer = null }
const start = () => { if (!timer) { startTime = Date.now() timer = setInterval(() => { let currentCountdown = countdown - Math.floor((Date.now() - startTime + time) / 1000) if (currentCountdown > 0) { console.log('countdown: ', currentCountdown) } else { console.log('done') stop() } }, 1000) } }
|
这里我们依然使用了setInterval,但是只是用来每隔一秒读取倒计时,用来实现用户视图层的更新,实际与倒计时实现并无太大关联,我们可以封装一个倒计时
class Countdown { constructor(countdown) { this.lastTime = 0 this.startTime = null this.countdown = countdown this.stoped = true }
start() { this.startTime = Date.now() this.stoped = false }
stop() { this.lastTime += Date.now() - this.startTime this.stoped = true }
getCountdown() { let result if (this.stoped) { result = this.countdown - this.lastTime } else { result = this.countdown - (Date.now() - this.startTime) - this.lastTime } return result >= 0 ? result : 0 } }
|
一但我们创建了一个Countdown
实例,就可以随时通过调用实例的getCountdown
方法获取当前的倒计时。在未开始start
时,会返回倒计时总秒数;
开始倒计时后,我们可以指定一定的时间间隔读取当前的倒计时,如果你想每秒更新view的倒计时,可以每秒取样,或是更小的时间间隔;
停止倒计时后,我们只能读取到一个固定值,除非再次开启倒计时;
倒计时为0时,将一直返回0,表示倒计时完成,可以将此视为完成标志;
以上我们已经可以拿到准确的倒计时,不论是倒计时还未完成,还是已经完成返回0,都能准确表示当前倒计时状态,但还是存在一个问题,就是我们无法知道倒计时是何时完成的,即便我们读取倒计时的值为0时,只能说明倒计时已经完成,却无法准确知道合适完成
所以这里还需要补充一个完成回调,来告诉我们倒计时何时完成,以便执行后续的业务逻辑
class Countdown { constructor(countdown, callback) { this.lastTime = 0 this.startTime = null this.countdown = countdown this.callback = callback this.timer = null }
start() { this.startTime = Date.now() this.timer = setTimeout(() => { this.callback && this.callback() }, this.countdown - this.lastTime) }
stop() { this.lastTime += Date.now() - this.startTime if (this.timer) { clearTimeout(this.timer) this.timer = null } }
getCountdown() { let result if (this.timer) { const currentTime = Date.now() - this.startTime + this.lastTime result = this.countdown - currentTime } else { result = this.countdown - this.lastTime } return result >= 0 ? result : 0 } }
|