研究大佬写的倒计时组件(Vue),学到了不少东西

开发 前端
入职的第一个需求是跟着一位前端大佬一起完成的一个活动项目。由于是一起开发,喜欢阅读源码的我,当然不会放过阅读大佬的代码的机会。

[[412028]]

一、前言

入职的第一个需求是跟着一位前端大佬一起完成的一个活动项目。

由于是一起开发,喜欢阅读源码的我,当然不会放过阅读大佬的代码的机会。

因为我的页面中需要使用到倒计时功能,发现大佬的已经写了个现成的倒计时组件,于是直接就拿过来用了。

传个参数就实现了功能的感觉真是太棒了。项目完成后,就膜拜了一下大佬的倒计时组件的代码。真是让我学到了不少。列举如下:

  1. 计时器为什么要用setTimeout而不用setInterval
  2. 为什么不直接将剩余时间-1。
  3. 如何将所需要的时间返回出去(有可能我只需要分钟和秒数,那就只返回分钟和秒数,也有可能我全都要)。
  4. 不确定接口返回的是剩余时间还是截止日期,该怎么同时兼容这两种情况。
  5. 不确定接口返回的时间是秒还是毫秒单位。

好了,你可能不太理解这些问题,但是没关系,看完下面的解释,相信你会豁然开朗。

二、开始手操

1. 先创建一个vue组件

  1. <template> 
  2.   <div class="_base-count-down"
  3.   </div> 
  4. </template> 
  5. <script> 
  6.  
  7. export default { 
  8.   data: () => ({ 
  9.     
  10.   }), 
  11.   props: { 
  12.      
  13.   }, 
  14. }; 
  15. </script> 
  16. <style lang='scss' scoped> 
  17.  
  18. </style> 

2. 实现基本的倒计时组件

接下来,假设接口获得的是一个剩余时间。

将剩余时间time传入这个倒计时组件,由于time可能是秒为单位的,也有可能是毫秒为单位的,所以我们需要在传入time的是有也传入一个isMilliSecond来告诉倒计时组件这个time是毫秒还是秒为单位的。如下代码中的props所示。

  1. <template> 
  2.   <div class="_base-count-down"
  3.   </div> 
  4. </template> 
  5. <script> 
  6.  
  7. export default { 
  8.   data: () => ({ 
  9.   }), 
  10.   props: { 
  11.     time: { 
  12.       type: [Number, String], 
  13.       default: 0 
  14.     }, 
  15.     isMilliSecond: { 
  16.       type: Boolean, 
  17.       defaultfalse 
  18.     } 
  19.   }, 
  20.   computed: { 
  21.     duration() { 
  22.       const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  23.       return time
  24.     } 
  25.   }, 
  26. }; 
  27. </script> 
  28. <style lang='scss' scoped> 
  29.  
  30. </style> 

computed中的duration是将time进行转化的结果,不管time是毫秒还是秒,都转化为秒 不知道你注意到了没有:+this.time。为什么要在前面加个‘+’号。这点很值得我们学习,因为接口返回的一串数字有时候是字符串的形式,有时候是数字的形式(不能过分相信后端同学,必须自己做好防范)。所以通过前面加个‘+’号 通通转化为数字。现在的duration就是转化后的time啦!

我们获得duration之后就可以开始倒计时了

  1. <template> 
  2.   <div class="_base-count-down"
  3.   </div> 
  4. </template> 
  5. <script> 
  6.  
  7. export default { 
  8.   data: () => ({ 
  9.   }), 
  10.   props: { 
  11.     time: { 
  12.       type: [Number, String], 
  13.       default: 0 
  14.     }, 
  15.     isMilliSecond: { 
  16.       type: Boolean, 
  17.       defaultfalse 
  18.     } 
  19.   }, 
  20.   computed: { 
  21.     duration() { 
  22.       const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  23.       return time
  24.     } 
  25.   }, 
  26.   // 新增代码: 
  27.   mounted() { 
  28.     this.countDown(); 
  29.   }, 
  30.   methods: { 
  31.     countDown() { 
  32.       this.getTime(this.duration); 
  33.     }, 
  34.   } 
  35. }; 
  36. </script> 
  37. <style lang='scss' scoped> 
  38.  
  39. </style> 

在这里创建了一个countDown方法,表示开始倒计时的意思,已进入页面就开始执行countdown方法。

countDown方法调用了getTime方法,getTime需要传入duration这个参数,也就是我们获得的剩余时间。

现在来实现一下这个方法。

  1. <template> 
  2.   <div class="_base-count-down"
  3.     还剩{{day}}天{{hours}}:{{mins}}:{{seconds}} 
  4.   </div> 
  5. </template> 
  6. <script> 
  7.  
  8. export default { 
  9.   data: () => ({ 
  10.     days: '0'
  11.     hours: '00'
  12.     mins: '00'
  13.     seconds: '00'
  14.     timer: null
  15.   }), 
  16.   props: { 
  17.     time: { 
  18.       type: [Number, String], 
  19.       default: 0 
  20.     }, 
  21.     isMilliSecond: { 
  22.       type: Boolean, 
  23.       defaultfalse 
  24.     } 
  25.   }, 
  26.   computed: { 
  27.     duration() { 
  28.       const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  29.       return time
  30.     } 
  31.   }, 
  32.   mounted() { 
  33.     this.countDown(); 
  34.   }, 
  35.   methods: { 
  36.     countDown() { 
  37.       this.getTime(this.duration); 
  38.     }, 
  39.     // 新增代码: 
  40.     getTime(duration) { 
  41.       this.timer && clearTimeout(this.timer); 
  42.       if (duration < 0) { 
  43.         return
  44.       } 
  45.       const { dd, hh, mm, ss } = this.durationFormatter(duration); 
  46.       this.days = dd || 0; 
  47.       this.hours = hh || 0; 
  48.       this.mins = mm || 0; 
  49.       this.seconds = ss || 0; 
  50.       this.timer = setTimeout(() => { 
  51.         this.getTime(duration - 1); 
  52.       }, 1000); 
  53.     } 
  54.   } 
  55. }; 
  56. </script> 
  57. <style lang='scss' scoped> 
  58.  
  59. </style> 

可以看到,getTime的目的就是获得 days,hours,mins,seconds,然后显示到html上,并且通过定时器实时来刷新days,hours,mins,seconds这个几个值。从而实现了倒计时。很简单,有木有?

durationFormatter是一个将duration转化成天数,小时,分钟,秒数的方法,很简单,可以看下它的具体实现。

  1. durationFormatter(time) { 
  2.   if (!timereturn { ss: 0 }; 
  3.   let t = time
  4.   const ss = t % 60; 
  5.   t = (t - ss) / 60; 
  6.   if (t < 1) return { ss }; 
  7.   const mm = t % 60; 
  8.   t = (t - mm) / 60; 
  9.   if (t < 1) return { mm, ss }; 
  10.   const hh = t % 24; 
  11.   t = (t - hh) / 24; 
  12.   if (t < 1) return { hh, mm, ss }; 
  13.   const dd = t; 
  14.   return { dd, hh, mm, ss }; 
  15. }, 

好了,问题开始来了!!

3. 为什么要用setTimeout来模拟setInterval的行为?

这里用setInerval不是更方便吗?

  1. setTimeout(function(){··· }, n); // n毫秒后执行function 
  1. setInterval(function(){··· }, n); // 每隔n毫秒执行一次function 

可以看看setInterval有什么缺点:

再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。

  1. setInterval(function, N)   
  2. //即:每隔N秒把function事件推到消息队列中 
图片

上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加,结果就是此时被跳过;这里我们可以看到,T1定时器执行结束后马上执行了T2代码,所以并没有达到定时器的效果。

综上所述,setInterval有两个缺点:

  1. 使用setInterval时,某些间隔会被跳过;
  2. 可能多个定时器会连续执行;

可以这么理解:每个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()。

代码如下:

  1. <template> 
  2.   <div class="_base-count-down"
  3.     还剩{{day}}天{{hours}}:{{mins}}:{{seconds}} 
  4.   </div> 
  5. </template> 
  6. <script> 
  7.  
  8. export default { 
  9.   data: () => ({ 
  10.     days: '0'
  11.     hours: '00'
  12.     mins: '00'
  13.     seconds: '00'
  14.     timer: null
  15.   }), 
  16.   props: { 
  17.     time: { 
  18.       type: [Number, String], 
  19.       default: 0 
  20.     }, 
  21.     isMilliSecond: { 
  22.       type: Boolean, 
  23.       defaultfalse 
  24.     } 
  25.   }, 
  26.   computed: { 
  27.     duration() { 
  28.       const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  29.       return time
  30.     } 
  31.   }, 
  32.   mounted() { 
  33.     this.countDown(); 
  34.   }, 
  35.   // 新增代码: 
  36.   watch: { 
  37.     duration() { 
  38.       this.countDown(); 
  39.     } 
  40.   }, 
  41.   methods: { 
  42.     countDown() { 
  43.       this.getTime(this.duration); 
  44.     }, 
  45.     durationFormatter(){...} 
  46.     getTime(duration) { 
  47.       this.timer && clearTimeout(this.timer); 
  48.       if (duration < 0) { 
  49.         return
  50.       } 
  51.       const { dd, hh, mm, ss } = this.durationFormatter(duration); 
  52.       this.days = dd || 0; 
  53.       this.hours = hh || 0; 
  54.       this.mins = mm || 0; 
  55.       this.seconds = ss || 0; 
  56.       this.timer = setTimeout(() => { 
  57.         this.getTime(duration - 1); 
  58.       }, 1000); 
  59.     } 
  60.   } 
  61. }; 
  62. </script> 
  63. <style lang='scss' scoped> 
  64.  
  65. </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方法。

  1. <template> 
  2.   <div class="_base-count-down"
  3.     还剩{{day}}天{{hours}}:{{mins}}:{{seconds}} 
  4.   </div> 
  5. </template> 
  6. <script> 
  7.  
  8. export default { 
  9.   data: () => ({ 
  10.     days: '0'
  11.     hours: '00'
  12.     mins: '00'
  13.     seconds: '00'
  14.     timer: null
  15.     curTime: 0,// 新增代码: 
  16.   }), 
  17.   props: { 
  18.     time: { 
  19.       type: [Number, String], 
  20.       default: 0 
  21.     }, 
  22.     isMilliSecond: { 
  23.       type: Boolean, 
  24.       defaultfalse 
  25.     } 
  26.   }, 
  27.   computed: { 
  28.     duration() { 
  29.       const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  30.       return time
  31.     } 
  32.   }, 
  33.   mounted() { 
  34.     this.countDown(); 
  35.   }, 
  36.    
  37.   watch: { 
  38.     duration() { 
  39.       this.countDown(); 
  40.     } 
  41.   }, 
  42.   methods: { 
  43.     countDown() { 
  44.       // 新增代码: 
  45.       this.curTime = Date.now(); 
  46.       this.getTime(this.duration); 
  47.     }, 
  48.     durationFormatter(){...} 
  49.     getTime(duration) { 
  50.       this.timer && clearTimeout(this.timer); 
  51.       if (duration < 0) { 
  52.         return
  53.       } 
  54.       const { dd, hh, mm, ss } = this.durationFormatter(duration); 
  55.       this.days = dd || 0; 
  56.       this.hours = hh || 0; 
  57.       this.mins = mm || 0; 
  58.       this.seconds = ss || 0; 
  59.       this.timer = setTimeout(() => { 
  60.         // 新增代码: 
  61.         const now = Date.now(); 
  62.         const diffTime = Math.floor((now - this.curTime) / 1000); 
  63.         this.curTime = now; 
  64.         this.getTime(duration - diffTime); 
  65.       }, 1000); 
  66.     } 
  67.   } 
  68. }; 
  69. </script> 
  70. <style lang='scss' scoped> 
  71.  
  72. </style> 

可以看到,我们在三个位置添加了新的代码。

首先在data了添加了curTime这个变量,然后在执行countDown的时候给curTime赋值Date.now(),也就是当前的时刻,也就是显示在页面上的那个时刻。

然后看修改的第三处代码。可以看到是将-1改成了-diffTime。

now 是 setTimeout的回调函数执行的时候的那个时刻。

因而 diffTime 则 表示 当前这个setTimeout的回调函数执行的时刻距离上 页面上的剩余时间上一次变化的时间段。其实也就是 当前这个setTimeout的回调函数执行的时刻距离上 一个setTimeout的回调函数执行的时刻时间段。

可能你还是不太能理解diffTime。举个例子:

你打开了这个倒计时页面,于是执行了countDown,也就是说要执行getTime这个方法了。也就是会马上执行下列的代码。

  1. this.days = dd || 0; 
  2. this.hours = hh || 0; 
  3. this.mins = mm || 0; 
  4. 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,就会重新执行下面的代码,有渲染了新的剩余时间。

  1. this.days = dd || 0; 
  2. this.hours = hh || 0; 
  3. this.mins = mm || 0; 
  4. 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就好了。

  1. computed: { 
  2.   duration() { 
  3.     if (this.end) { 
  4.       let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000; 
  5.       end -= Date.now(); 
  6.       return end
  7.     } 
  8.     const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  9.     return time
  10.   } 
  11. }, 

判断传入的end的长度是否大于13来判断是秒还是毫秒。轻松!

7. 添加新功能:可以选择要显示的内容,例如只显示秒,或者只显示小时。

只需要改动一下html:

  1. <template> 
  2.   <div class="_base-count-down no-rtl"
  3.     <div class="content"
  4.       <slot v-bind="{ 
  5.         d: days, h: hours, m: mins, s: seconds, 
  6.         hh: `00${hours}`.slice(-2), 
  7.         mm: `00${mins}`.slice(-2), 
  8.         ss: `00${seconds}`.slice(-2), 
  9.       }"></slot> 
  10.     </div> 
  11.   </div> 
  12. </template> 

很巧妙有没有,只需要用插槽,就把倒计时组件,也就是把子组件的值传递给父组件了。

看看父组件是怎么使用这个组件的。

  1. <base-counter v-slot="timeObj" :time="countDown"
  2.   <div class="count-down"
  3.     <div class="icon"></div> 
  4.     {{timeObj.d}}天{{timeObj.hh}}小时{{timeObj.mm}}分钟{{timeObj.ss}}秒 
  5.   </div> 
  6. </base-counter> 

看,如此巧妙又简单。

发现00${hours}.slice(-2) 这种写法也很值得学习。以前在获得到分钟的时候,要手动判断获得的分钟是两位数还是一位数,如果是一位数的话就要在前面手动补上0。就像下面的代码:

  1. var StartMinute = startDate.getMinutes().toString().length >= 2 ? startDate.getMinutes() : '0' + startDate.getHours(); 

而00${hours}.slice(-2) 则不用判断,先补上0再说,然后再从后面往前截取两位。

[[412030]]

到此。

一个完美的倒计时组件就完成了。

三、学习总结

  1. 明白了setInterval的缺点以及用setTimeout代替setInterval。
  2. 学到了“+”,操作,不管三七二十一,将接口得到的长串数字转化为数字保平安。
  3. 利用clearTimeout来清除掉之前的计时器,以防止造成影响。
  4. 学会使用v-slot来子传父传值
  5. 学会一个倒计时组件,为了以后方便cv操作。把组件完整代码贴上:
  1. <template> 
  2.   <div class="_base-count-down no-rtl"
  3.     <div class="content"
  4.       <slot v-bind="{ 
  5.         d: days, h: hours, m: mins, s: seconds, 
  6.         hh: `00${hours}`.slice(-2), 
  7.         mm: `00${mins}`.slice(-2), 
  8.         ss: `00${seconds}`.slice(-2), 
  9.       }"></slot> 
  10.     </div> 
  11.   </div> 
  12. </template> 
  13. <script> 
  14. /* eslint-disable object-curly-newline */ 
  15.  
  16. export default { 
  17.   data: () => ({ 
  18.     days: '0'
  19.     hours: '00'
  20.     mins: '00'
  21.     seconds: '00'
  22.     timer: null
  23.     curTime: 0 
  24.   }), 
  25.   props: { 
  26.     time: { 
  27.       type: [Number, String], 
  28.       default: 0 
  29.     }, 
  30.     refreshCounter: { 
  31.       type: [Number, String], 
  32.       default: 0 
  33.     }, 
  34.     end: { 
  35.       type: [Number, String], 
  36.       default: 0 
  37.     }, 
  38.     isMiniSecond: { 
  39.       type: Boolean, 
  40.       defaultfalse 
  41.     } 
  42.   }, 
  43.   computed: { 
  44.     duration() { 
  45.       if (this.end) { 
  46.         let end = String(this.end).length >= 13 ? +this.end : +this.end * 1000; 
  47.         end -= Date.now(); 
  48.         return end
  49.       } 
  50.       const time = this.isMiniSecond ? Math.round(+this.time / 1000) : Math.round(+this.time); 
  51.       return time
  52.     } 
  53.   }, 
  54.   mounted() { 
  55.     this.countDown(); 
  56.   }, 
  57.   watch: { 
  58.     duration() { 
  59.       this.countDown(); 
  60.     }, 
  61.     refreshCounter() { 
  62.       this.countDown(); 
  63.     } 
  64.   }, 
  65.   methods: { 
  66.     durationFormatter(time) { 
  67.       if (!timereturn { ss: 0 }; 
  68.       let t = time
  69.       const ss = t % 60; 
  70.       t = (t - ss) / 60; 
  71.       if (t < 1) return { ss }; 
  72.       const mm = t % 60; 
  73.       t = (t - mm) / 60; 
  74.       if (t < 1) return { mm, ss }; 
  75.       const hh = t % 24; 
  76.       t = (t - hh) / 24; 
  77.       if (t < 1) return { hh, mm, ss }; 
  78.       const dd = t; 
  79.       return { dd, hh, mm, ss }; 
  80.     }, 
  81.     countDown() { 
  82.       // eslint-disable-next-line no-unused-expressions 
  83.       this.curTime = Date.now(); 
  84.       this.getTime(this.duration); 
  85.     }, 
  86.     getTime(time) { 
  87.       // eslint-disable-next-line no-unused-expressions 
  88.       this.timer && clearTimeout(this.timer); 
  89.       if (time < 0) { 
  90.         return
  91.       } 
  92.       // eslint-disable-next-line object-curly-newline 
  93.       const { dd, hh, mm, ss } = this.durationFormatter(time); 
  94.       this.days = dd || 0; 
  95.       // this.hours = `00${hh || ''}`.slice(-2); 
  96.       // this.mins = `00${mm || ''}`.slice(-2); 
  97.       // this.seconds = `00${ss || ''}`.slice(-2); 
  98.       this.hours = hh || 0; 
  99.       this.mins = mm || 0; 
  100.       this.seconds = ss || 0; 
  101.       this.timer = setTimeout(() => { 
  102.         const now = Date.now(); 
  103.         const diffTime = Math.floor((now - this.curTime) / 1000); 
  104.         const step = diffTime > 1 ? diffTime : 1; // 页面退到后台的时候不会计时,对比时间差,大于1s的重置倒计时 
  105.         this.curTime = now; 
  106.         this.getTime(time - step); 
  107.       }, 1000); 
  108.     } 
  109.   } 
  110. }; 
  111. </script> 
  112. <style lang='scss' scoped> 
  113. @import '~@assets/css/common.scss'
  114.  
  115. ._base-count-down { 
  116.   color: #fff; 
  117.   text-align: left
  118.   position: relative
  119.   .content { 
  120.     width: auto; 
  121.     display: flex; 
  122.     align-items: center; 
  123.   } 
  124.   span { 
  125.     display: inline-block; 
  126.   } 
  127.   .section { 
  128.     position: relative
  129.   } 
  130. </style> 

 

责任编辑:姜华 来源: 前端阳光
相关推荐

2014-08-18 14:30:27

Android倒计时

2022-10-21 15:42:21

倒计时鸿蒙

2014-03-21 13:46:45

2011-04-11 09:17:28

Ubuntu倒计时

2017-07-20 16:21:52

UICountDownTidelay

2015-03-23 17:58:04

验证码倒计时并行

2014-02-18 10:36:33

2011-04-11 09:50:56

Ubuntu 11.0

2022-06-14 08:45:27

浏览器IEWindows

2013-10-08 09:24:39

Windows 8.1Windows 8

2020-10-28 17:54:49

成都信息安全

2013-04-09 10:01:18

微软Windows XP

2013-10-10 09:23:15

Android 4.4Kitkat

2019-12-13 19:37:00

BashLinux命令

2011-05-23 08:43:40

jQueryjQuery插件

2011-03-06 15:49:25

webOSBlackBerry

2012-12-28 13:50:00

2012-03-28 09:37:07

Ubuntu 12.0倒计时

2015-01-21 16:07:57

Android源码验证码倒计时

2013-06-06 11:27:52

iRadioWWDC2013
点赞
收藏

51CTO技术栈公众号