二、开始手操

  1. 先创立一个vue组件
    <template>
    <div class="_base-count-down">
    </div>
    </template>
    <script>

export default {
data: () => ({

}),
props: {

},
};
</script>
<style lang='scss' scoped>

</style>
复制代码

  1. 实现根本的倒计时组件
    接下来,假如接口取得的是一个剩余时间。
    将剩余时间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 };
},
复制代码
好了,问题开始来了!!

  1. 为什么要用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,来躲避掉下面的毛病。

  1. 为什么要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革除掉。

  1. 应用 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);
这样就完满解决了因为浏览器切到后盾,导致剩余时间不变的问题。

  1. 增加新性能:能够传入到期工夫。
    之前是只能传入剩余时间的,当初心愿也反对传入到期工夫。
    只须要改变一下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来判断是秒还是毫秒。轻松!

  2. 增加新性能:能够抉择要显示的内容,例如只显示秒,或者只显示小时。
    只须要改变一下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>