0%

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 {
// 传入倒计时初始值(ms)
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 {
// 传入倒计时初始值(ms)及完成回调
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
}
}

1.常规用法

单选按钮通常用于用户在一组选项中选中某一项的场景中,一但用户选中其中一项,是不可以再次点击以取消选中的。

常见的写法有以下几种

无label的

<input type="radio" name="option" value="a" checked>选项A
<input type="radio" name="option" value="b">选项B

通过for关联

<input type="radio" id="optionA" name="option2" value="a" checked><label for="optionA">选项A</label>
<input type="radio" id="optionB" name="option2" value="b"><label for="optionB">选项B</label>

或是直接将input放到对应的label中

<label><input type="radio" name="option3" value="a" checked>选项A</label>
<label><input type="radio" name="option3" value="b">选项B</label>

2.取消选中

如果有需求需要允许用户将已经选中的单选按钮取消选中,这个需求其实并不是非常常见,但也算有实际的需求场景,还算合理
但是默认提供的单选按钮是无法取消选中的,那么只有通过相应的事件进行处理了

对于一组单选按钮,他们是相互关联的,选中其一,则其他的会取消选中;那么我们可以监听单选的click事件
如果
1.已经选中,那么设置元素的checked = false,取消选中
2.未选中,那么设置元素的checked = true, 选中;同组的其它单选元素的checked = false

但在实际处理中会发现,我们并不知道click前的状态,因为点击后,我们事件的e.target.checked都会为true,所以我们应该记录下click之前的状态

const radioEls = document.querySelectorAll('input[type=radio]')
radioEls.forEach(el => {
el._checked = el.checked
el.addEventListener('click', e => {
const target = e.target
if (target._checked) {
target.checked = target._checked = false
} else {
document.querySelectorAll(`input[name=${target.name}]`).forEach(el => {
el.checked = el._checked = false
})
target.checked = target._checked = true
}
})
})

查看效果

解决办法

判断是否存在finally方法,不存在则在Promise原型上添加finally方法

// 兼容ios真机环境下Promise对象不存在finally方法
if (!Promise.prototype.finally) {
Promise.prototype.finally = function(callback) {
this.then(res => {
callback && callback(res)
}, error => {
callback && callback(error)
})
}
}

Promise finally实现

Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};