手把手带你分解 Vue 倒计时组件
大家好,我是漫步,今天来分享一个Vue 组件的内部,喜欢记得关注我并设为星标。
一、前言 前端开发博客
入职的第一个需求是跟着一位前端大佬一起完成的一个活动项目。
由于是一起开发,当然不会放过阅读大佬的代码的机会。
因为我的页面中需要使用到倒计时功能,发现大佬的已经写了个现成的倒计时组件,于是直接就拿过来用了。
传个参数就实现了功能的感觉真是太棒了。项目完成后,就膜拜了一下大佬的倒计时组件的代码。真是让我学到了不少。列举如下:
计时器为什么要用setTimeout而不用setInterval
为什么不直接将剩余时间-1。
如何将所需要的时间返回出去(有可能我只需要分钟和秒数,那就只返回分钟和秒数,也有可能我全都要)。
不确定接口返回的是剩余时间还是截止日期,该怎么同时兼容这两种情况。
不确定接口返回的时间是秒还是毫秒单位。
好了,你可能不太理解这些问题,但是没关系,看完下面的解释,相信你会豁然开朗。
二、开始手操 前端开发博客
1. 先创建一个vue组件
<template><div class="_base-count-down"></div>
</template>
<script>export default {data: () => ({}),props: {},
};
</script>
<style lang='scss' scoped></style>
2. 实现基本的倒计时组件
接下来,假设接口获得的是一个剩余时间。
将剩余时间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 };
},
好了,问题开始来了!!
3. 为什么要用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,来规避掉上面的缺点。
4. 为什么要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清除掉。
5. 使用 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);
这样就完美解决了因为浏览器切到后台,导致剩余时间不变的问题。
6. 添加新功能:可以传入到期时间。
之前是只能传入剩余时间的,现在希望也支持传入到期时间。
只需要改动一下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来判断是秒还是毫秒。轻松!
7. 添加新功能:可以选择要显示的内容,例如只显示秒,或者只显示小时。
只需要改动一下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再说,然后再从后面往前截取两位。
到此。
一个完美的倒计时组件就完成了。
关于Vue一些技巧,你还可以看看:Vue实战中的一些小魔法
三、学习总结
明白了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-expressionsthis.curTime = Date.now();this.getTime(this.duration);},getTime(time) {// eslint-disable-next-line no-unused-expressionsthis.timer && clearTimeout(this.timer);if (time < 0) {return;}// eslint-disable-next-line object-curly-newlineconst { 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>
你学到了什么,欢迎补充!!
欢迎大家留言讨论,祝工作顺利、生活愉快!
我是bigo前端,下期见。
关于本文
作者:bigo前端
https://juejin.cn/post/7038405108371030047
推荐链接
前端程序员简历制作建议
前端程序员简历模板整理和下载
2021年前端开发博客文章推荐
关注下方「前端开发博客」,回复 “思维图”
获取公众号所有JS思维图
❤️ 看完两件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我两个小忙:
点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)
关注公众号「前端开发博客」,每周重点攻克一个前端面试重难点
如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~
手把手带你分解 Vue 倒计时组件相关推荐
- 手把手带你学会Odoo OWL组件开发(2):OWL的使用
[本系列内容直达:] [手把手带你学习Odoo OWL组件开发(1):认识 OWL] [手把手带你学会Odoo OWL组件开发(2):OWL的使用] [手把手带你学会Odoo OWL组件开发(3):核 ...
- 手把手带你学会Odoo OWL组件开发(5):浅析OWL原理
[本系列内容直达:] 手把手带你学习Odoo OWL组件开发(1):认识 OWL 手把手带你学会Odoo OWL组件开发(2):OWL的使用 手把手带你学会Odoo OWL组件开发(3):核心内容指南 ...
- 手把手带你学会Odoo OWL组件开发(4):OWL组件
[本系列内容直达:] 手把手带你学习Odoo OWL组件开发(1):认识 OWL 手把手带你学会Odoo OWL组件开发(2):OWL的使用 手把手带你学会Odoo OWL组件开发(3):核心内容指南 ...
- 封装Vue倒计时组件vuecountdown(详细教程)
由于开发需要,捣鼓了一个倒计时组件!特此分享,希望帮助到大家 先看效果 倒计时 来看流程,由于组件上传到远程仓库,大家可直接下载使用 npm下载 npm install --save-dev dirc ...
- 15 | 一起练习:手把手带你分解任务
前面在讨论 TDD 的时候,我们说任务分解是 TDD 的关键.但这依然是一种感性上的认识.今天,我们就来用一个更加具体的例子,让你看看任务分解到底可以做到什么程度. 这个例子就是最简单的用户登录.需求 ...
- 手把手带你使用Vue实现一个图片水平瀑布流插件
如何使用Vue实现一个图片水平瀑布流插件?这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助. 一.需求来源 今天碰到了一个需求,需要在页面里,用水平瀑布流的方式,将一些图片进行加载,这让我 ...
- Vue实现倒计时组件(可自定义时间倒计时功能的组件):
一.创建countDown.vue(倒计时组件): <template><span :endTime="endTime" :endText="endTe ...
- 手把手撸一个小而美的日历组件
手把手撸一个小而美的日历组件 前言 日历是前端开发中常见的业务组件之一,虽然现在有很多现成的日历组件,但是呢很多时候需要定制的时候就需要我们自己造一个,此时我们便需要了解日历的生成原理.其实也没有想象 ...
- 手把手带你从零打造Vue SSR,清晰易懂!
Vue SSR,服务端渲染,优点大家都很清楚,能大大提升首屏渲染速度,优化用户体验,还有利于SEO. 但说实话,Vue SSR并不好上手.官网给的例子大而全,太复杂.而网上很多从0到1打造Vue SS ...
最新文章
- 解读《电力发展“十三五”规划》
- python求向量函数的雅可比矩阵_在python Numpy中求向量和矩阵的范数实例
- string类型加减_测试人员应该知道的Redis知识(四) String
- 孙兴慜已飞抵阿联酋 能否出战国足将视情况而定
- Django modules模块
- 让你的silverlight更炫(三):让BusyIndicator更炫
- 设计模式——开发常用的设计模式梳理
- 数据导出到Excel
- android fragment中引入自定义view_厉害了,用Android自定义View实现八大行星绕太阳3D旋转效果...
- 运用数组,输出从小到大的10个数
- C#LeetCode刷题之#414-第三大的数(Third Maximum Number)
- 在Django将已有数据库生成models文件
- HDU(1175),连连看,BFS
- 编程基本功:变量局部化的教训
- Maven默认的生命周期
- 无穷级数求和7个公式_高中数学:教你等差数列求和公式,有这7种方法
- 计算机对舞蹈影响,计算机技术在舞蹈教学中的应用
- 利用supervisor对服务进行管理
- wappush系统框架
- 2018年10月训练记录(10.1~10.23)