二、开始手操
- 先创立一个 vue 组件
<template>
<div class=”_base-count-down”>
</div>
</template>
<script>
export default {
data: () => ({
}),
props: {
},
};
</script>
<style lang=’scss’ scoped>
</style>
复制代码
- 实现根本的倒计时组件
接下来,假如接口取得的是一个剩余时间。
将剩余时间 time 传入这个倒计时组件,因为 time 可能是秒为单位的,也有可能是毫秒为单位的,所以咱们须要在传入 time 的是有也传入一个 isMilliSecond 来通知倒计时组件这个 time 是毫秒还是秒为单位的。如下代码中的 props 所示。
<template>
<div class=”_base-count-down”>
</div>
</template>
<script>
export default {
data: () => ({
}),
props: {
time: {type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
};
</script>
<style lang=’scss’ scoped>
</style>
复制代码
computed 中的 duration 是将 time 进行转化的后果,不论 time 是毫秒还是秒,都转化为秒
不晓得你留神到了没有:+this.time。为什么要在后面加个‘+’号。这点很值得咱们学习,因为接口返回的一串数字有时候是字符串的模式,有时候是数字的模式(不能过分置信后端同学,必须本人做好防备)。所以通过后面加个‘+’号 统统转化为数字。当初的 duration 就是转化后的 time 啦!
咱们取得 duration 之后就能够开始倒计时了
<template>
<div class=”_base-count-down”>
</div>
</template>
<script>
export default {
data: () => ({
}),
props: {
time: {type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
// 新增代码:
mounted() {
this.countDown();
},
methods: {
countDown() {this.getTime(this.duration);
},
}
};
</script>
<style lang=’scss’ scoped>
</style>
复制代码
在这里创立了一个 countDown 办法,示意开始倒计时的意思,已进入页面就开始执行 countdown 办法。
countDown 办法调用了 getTime 办法,getTime 须要传入 duration 这个参数,也就是咱们取得的剩余时间。
当初来实现一下这个办法。
<template>
<div class=”_base-count-down”>
还剩 {{day}} 天{{hours}}:{{mins}}:{{seconds}}
</div>
</template>
<script>
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
}),
props: {
time: {type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
methods: {
countDown() {this.getTime(this.duration);
},
// 新增代码:getTime(duration) {this.timer && clearTimeout(this.timer);
if (duration < 0) {return;}
const {dd, hh, mm, ss} = this.durationFormatter(duration);
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {this.getTime(duration - 1);
}, 1000);
}
}
};
</script>
<style lang=’scss’ scoped>
</style>
复制代码
能够看到,getTime 的目标就是取得 days,hours,mins,seconds,而后显示到 html 上,并且通过定时器实时来刷新 days,hours,mins,seconds 这个几个值。从而实现了倒计时。很简略,有木有?
durationFormatter 是一个将 duration 转化成天数,小时,分钟,秒数的办法,很简略,能够看下它的具体实现。
durationFormatter(time) {
if (!time) return {ss: 0};
let t = time;
const ss = t % 60;
t = (t – ss) / 60;
if (t < 1) return {ss};
const mm = t % 60;
t = (t – mm) / 60;
if (t < 1) return {mm, ss};
const hh = t % 24;
t = (t – hh) / 24;
if (t < 1) return {hh, mm, ss};
const dd = t;
return {dd, hh, mm, ss};
},
复制代码
好了,问题开始来了!!
- 为什么要用 setTimeout 来模仿 setInterval 的行为?
这里用 setInerval 不是更不便吗?
setTimeout(function(){···}, n); // n 毫秒后执行 function
复制代码
setInterval(function(){···}, n); // 每隔 n 毫秒执行一次 function
复制代码
能够看看 setInterval 有什么毛病:
再次强调,定时器指定的工夫距离,示意的是何时将定时器的代码增加到音讯队列,而不是何时执行代码。所以真正何时执行代码的工夫是不能保障的,取决于何时被主线程的事件循环取到,并执行。
setInterval(function, N)
// 即:每隔 N 秒把 function 事件推到音讯队列中
复制代码
上图可见,setInterval 每隔 100ms 往队列中增加一个事件;100ms 后,增加 T1 定时器代码至队列中,主线程中还有工作在执行,所以期待,some event 执行完结后执行 T1 定时器代码;又过了 100ms,T2 定时器被增加到队列中,主线程还在执行 T1 代码,所以期待;又过了 100ms,实践上又要往队列里推一个定时器代码,但因为此时 T2 还在队列中,所以 T3 不会被增加,后果就是此时被跳过;这里咱们能够看到,T1 定时器执行完结后马上执行了 T2 代码,所以并没有达到定时器的成果。
综上所述,setInterval 有两个毛病:
应用 setInterval 时,某些距离会被跳过;
可能多个定时器会间断执行;
能够这么了解:每个 setTimeout 产生的工作会间接 push 到工作队列中;而 setInterval 在每次把工作 push 到工作队列前,都要进行一下判断 (看上次的工作是否仍在队列中)。
因此咱们个别用 setTimeout 模仿 setInterval,来躲避掉下面的毛病。
- 为什么要 clearTimeout(this.timer)
第二问:为什么要有 this.timer && clearTimeout(this.timer); 这一句?
假如一个场景:
如图所示,在倒计时的父组件中,有两个按钮,点击流动一就会传入流动一的剩余时间,点击流动二,就会传入流动二的工夫。
如果此时倒计时组件正在做流动一的倒计时,而后点击流动二,就要会马上传入新的 time,这个时候就须要从新计时。当然,这里并不会从新计时,因为组件的 mounted 只会执行一次。也就是说 this.countDown(); 只会执行一次,也就是说 this.getTime(this.duration); 只会执行一次,因而 duration 还是流动一的工夫,怎么办呢?watch 派上用场了。
咱们来监听 duration,如果发现 duration 变动,阐明新的工夫 time 传入组件,这时就要从新调用 this.countDown()。
代码如下:
<template>
<div class=”_base-count-down”>
还剩 {{day}} 天{{hours}}:{{mins}}:{{seconds}}
</div>
</template>
<script>
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
}),
props: {
time: {type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
// 新增代码:
watch: {
duration() {this.countDown();
}
},
methods: {
countDown() {this.getTime(this.duration);
},
durationFormatter(){...}
getTime(duration) {this.timer && clearTimeout(this.timer);
if (duration < 0) {return;}
const {dd, hh, mm, ss} = this.durationFormatter(duration);
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {this.getTime(duration - 1);
}, 1000);
}
}
};
</script>
<style lang=’scss’ scoped>
</style>
复制代码
好了,然而并没有解释下面提出的那个问题:为什么要有 this.timer && clearTimeout(this.timer); 这一句?
这样,假如当初页面显示的是流动一的工夫,这时,执行到 setTimeout,在一秒后就会把 setTimeout 里的回调函数放到工作队列中,留神是一秒后哦!这时,然而,在这一秒的结尾,咱们点击了流动二按钮,这时候的流动二的工夫就会传入倒计时组件中,而后触发 countDown(), 也就调用 this.getTime(this.duration);,而后执行到 setTimeout,也会一秒后把回调函数放到工作队列中。
这时,工作队列中就会有两个 setTimeout 的回调函数了。期待一秒过来,两个回调函数相继执行,咱们就会看到页面上的工夫一下子背减了 2,实际上是很疾速地进行了两遍减 1 的操作。
这就是为什么要增加上 this.timer && clearTimeout(this.timer); 这一句的起因了。就是要把上一个 setTimeout 革除掉。
- 应用 diffTime
当你认为这是一个完满的组件的时候,你想把这个组件用到我的项目上,假如你也的确用了,而且还上线了,确发现呈现了个大问题:当页面关上的时候,倒计时开始了,工夫是 还剩 1 天 12:25:25,而后有人给你发微信,你马上切换到微信,回复音讯后切回浏览器,发现倒计时工夫却还是还剩 1 天 12:25:25。你慌了:你写的代码呈现 bug 了!
这是怎么回事?
出于节能的思考, 局部浏览器在进入后盾时 (或者失去焦点时), 会将 setTimeout 等定时工作暂停
待用户回到浏览器时, 才会从新激活定时工作
说是暂停, 其实应该说是提早, 1s 的工作提早到 2s, 2s 的提早到 5s, 理论状况因浏览器而异。
原来如此,看来不能每次都只是减 1 这么简略了(毕竟你把浏览器切到后盾之后 setTimeout 就冷却了,等几秒后切回,而后执行 setTimeout,只是减了一秒而已)。
所以咱们须要改写一下 getTime 办法。
<template>
<div class=”_base-count-down”>
还剩 {{day}} 天{{hours}}:{{mins}}:{{seconds}}
</div>
</template>
<script>
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
curTime: 0,// 新增代码:
}),
props: {
time: {type: [Number, String],
default: 0
},
isMilliSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
watch: {
duration() {this.countDown();
}
},
methods: {
countDown() {
// 新增代码:this.curTime = Date.now();
this.getTime(this.duration);
},
durationFormatter(){...}
getTime(duration) {this.timer && clearTimeout(this.timer);
if (duration < 0) {return;}
const {dd, hh, mm, ss} = this.durationFormatter(duration);
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {
// 新增代码:const now = Date.now();
const diffTime = Math.floor((now - this.curTime) / 1000);
this.curTime = now;
this.getTime(duration - diffTime);
}, 1000);
}
}
};
</script>
<style lang=’scss’ scoped>
</style>
复制代码
能够看到,咱们在三个地位增加了新的代码。
首先在 data 了增加了 curTime 这个变量,而后在执行 countDown 的时候给 curTime 赋值 Date.now(),也就是以后的时刻,也就是显示在页面上的那个时刻。
而后看批改的第三处代码。能够看到是将 - 1 改成了 -diffTime。
now 是 setTimeout 的回调函数执行的时候的那个时刻。
因此 diffTime 则 示意 以后这个 setTimeout 的回调函数执行的时刻间隔上 页面上的剩余时间上一次变动的时间段。其实也就是 以后这个 setTimeout 的回调函数执行的时刻间隔上 一个 setTimeout 的回调函数执行的时刻时间段。
可能你还是不太能了解 diffTime。举个例子:
你关上了这个倒计时页面,于是执行了 countDown,也就是说要执行 getTime 这个办法了。也就是会马上执行下列的代码。
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
复制代码
执行完这些代码页面上就会呈现剩余时间。
而 this.curTime = Date.now(); 就记录下了此刻的工夫点。
而后一秒后执行 setTimeout 里的回调函数:
const now = Date.now(); 记录以后这个 setTimeout 的回调函数执行的工夫点。
const diffTime = Math.floor((now – this.curTime) / 1000); 记录以后这个 setTimeout 的回调函数执行的工夫点间隔页面上开始 渲染 剩余时间的 这一段时间。其实此时的 diffTime 就是 =1。
而后 this.curTime = now; 将 curTime 的值变成以后这个 setTimeout 的回调函数执行的工夫点。
this.getTime(duration – diffTime); 其实就是 this.getTime(duration – 1);
而后又执行 getTime,就会从新执行上面的代码,有渲染了新的剩余时间。
this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
复制代码
而后一秒后又要执行 setTmieout 的回调函数,在这一秒还没完结的时候,咱们将浏览器切到后盾,此时 setTimeout 冷却了。等 5 秒后再切回。于是 setTmieout 的回调函数才得以执行。
这时 const now = Date.now(); 记录以后这个 setTimeout 的回调函数执行的工夫点。
而 curTime 是上一个 setTimeout 的回调函数执行的工夫。
所以 const diffTime = Math.floor((now – this.curTime) / 1000); 实际上,diffTime 的值就是 5 秒。
因此 this.getTime(duration – diffTime); 其实就是 this.getTime(duration – 5);
这样就完满解决了因为浏览器切到后盾,导致剩余时间不变的问题。
-
增加新性能:能够传入到期工夫。
之前是只能传入剩余时间的,当初心愿也反对传入到期工夫。
只须要改变一下 duration 就好了。
computed: {
duration() {
if (this.end) {let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000; end -= Date.now(); return end;
}
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
复制代码
判断传入的 end 的长度是否大于 13 来判断是秒还是毫秒。轻松! -
增加新性能:能够抉择要显示的内容,例如只显示秒,或者只显示小时。
只须要改变一下 html:
<template>
<div class=”_base-count-down no-rtl”>
<div class=”content”>
<slot v-bind=”{d: days, h: hours, m: mins, s: seconds, hh: `00${hours}`.slice(-2), mm: `00${mins}`.slice(-2), ss: `00${seconds}`.slice(-2),
}”></slot>
</div>
</div>
</template>
复制代码
很奇妙有没有,只须要用插槽,就把倒计时组件,也就是把子组件的值传递给父组件了。
看看父组件是怎么应用这个组件的。
<base-counter v-slot=”timeObj” :time=”countDown”>
<div class=”count-down”>
<div class=”icon”></div>
{{timeObj.d}}天 {{timeObj.hh}} 小时 {{timeObj.mm}} 分钟 {{timeObj.ss}} 秒
</div>
</base-counter>
复制代码
看,如此奇妙又简略。
发现 00${hours}.slice(-2) 这种写法也很值得学习。以前在取得到分钟的时候,要手动判断取得的分钟是两位数还是一位数,如果是一位数的话就要在后面手动补上 0。就像上面的代码:
var StartMinute = startDate.getMinutes().toString().length >= 2 ? startDate.getMinutes() : ‘0’ + startDate.getHours();
复制代码
而 00${hours}.slice(-2) 则不必判断,先补上 0 再说,而后再从前面往前截取两位。
到此。
一个完满的倒计时组件就实现了。
三、学习总结
明确了 setInterval 的毛病以及用 setTimeout 代替 setInterval。
学到了“+”,操作,不管三七二十一,将接口失去的长串数字转化为数字保平安。
利用 clearTimeout 来革除掉之前的计时器,以避免造成影响。
学会应用 v -slot 来子传父传值
学会一个倒计时组件,为了当前不便 cv 操作。把组件残缺代码贴上:
<template>
<div class=”_base-count-down no-rtl”>
<div class="content">
<slot v-bind="{
d: days, h: hours, m: mins, s: seconds,
hh: `00${hours}`.slice(-2),
mm: `00${mins}`.slice(-2),
ss: `00${seconds}`.slice(-2),
}"></slot>
</div>
</div>
</template>
<script>
/ eslint-disable object-curly-newline /
export default {
data: () => ({
days: '0',
hours: '00',
mins: '00',
seconds: '00',
timer: null,
curTime: 0
}),
props: {
time: {type: [Number, String],
default: 0
},
refreshCounter: {type: [Number, String],
default: 0
},
end: {type: [Number, String],
default: 0
},
isMiniSecond: {
type: Boolean,
default: false
}
},
computed: {
duration() {if (this.end) {let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000;
end -= Date.now();
return end;
}
const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time);
return time;
}
},
mounted() {
this.countDown();
},
watch: {
duration() {this.countDown();
},
refreshCounter() {this.countDown();
}
},
methods: {
durationFormatter(time) {if (!time) return {ss: 0};
let t = time;
const ss = t % 60;
t = (t - ss) / 60;
if (t < 1) return {ss};
const mm = t % 60;
t = (t - mm) / 60;
if (t < 1) return {mm, ss};
const hh = t % 24;
t = (t - hh) / 24;
if (t < 1) return {hh, mm, ss};
const dd = t;
return {dd, hh, mm, ss};
},
countDown() {
// eslint-disable-next-line no-unused-expressions
this.curTime = Date.now();
this.getTime(this.duration);
},
getTime(time) {
// eslint-disable-next-line no-unused-expressions
this.timer && clearTimeout(this.timer);
if (time < 0) {return;}
// eslint-disable-next-line object-curly-newline
const {dd, hh, mm, ss} = this.durationFormatter(time);
this.days = dd || 0;
// this.hours = `00${hh || ''}`.slice(-2);
// this.mins = `00${mm || ''}`.slice(-2);
// this.seconds = `00${ss || ''}`.slice(-2);
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;
this.timer = setTimeout(() => {const now = Date.now();
const diffTime = Math.floor((now - this.curTime) / 1000);
const step = diffTime > 1 ? diffTime : 1; // 页面退到后盾的时候不会计时,比照时间差,大于 1s 的重置倒计时
this.curTime = now;
this.getTime(time - step);
}, 1000);
}
}
};
</script>
<style lang=’scss’ scoped>
@import ‘~@assets/css/common.scss’;
._base-count-down {
color: #fff;
text-align: left;
position: relative;
.content {
width: auto;
display: flex;
align-items: center;
}
span {
display: inline-block;
}
.section {
position: relative;
}
}
</style>