Getting Started with Vue Components: Countdown

file

This article was first published at: https://github.com/bigo-frontend/blog/ Welcome to follow and repost.

I. Introduction

The first requirement for entry is to complete an activity project with a front-end boss.

Since it is developed together, of course I will not miss the opportunity to read the big guy's code.

Because I need to use the countdown function in my page, I found that the boss has written a ready-made countdown component, so I directly used it.

It feels great to pass a parameter and realize the function. After the project was completed, I worshiped the code of the countdown component of the boss. It really taught me a lot. Listed below:

  1. Why do timers use setTimeout instead of setInterval
  2. Why not just -1 the remaining time.
  3. How to return the required time (maybe I only need the minutes and seconds, then only the minutes and seconds are returned, or it is possible that I want them all).
  4. It is not sure whether the interface returns the remaining time or the deadline, how to be compatible with both situations.
  5. It is not sure whether the time returned by the interface is in seconds or milliseconds.

Well, you may not understand these questions, but it doesn't matter, after reading the explanation below, I believe you will suddenly see the light.

2. Start manual operation

1. First create a vue component

<template>
  <div class="_base-count-down">
  </div>
</template>
<script>

export default {
      
      
  data: () => ({
      
      
   
  }),
  props: {
      
      
    
  },
};
</script>
<style lang='scss' scoped>

</style>

2. Implement the basic countdown component

Next, assume that what the interface obtains is a remaining time.

Pass the remaining time timeto the countdown component. Since time may be in seconds or milliseconds, we need to pass in timeone isMilliSecondto tell the countdown component whether it timeis in milliseconds or seconds. of. as shown in the code below 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>

computedThe duration in is the result of converting time, whether timeit is milliseconds or seconds, it is converted into seconds.
I don’t know if you have noticed: +this.time. Why add a ' + ' sign in front. This is worth learning, because the string of numbers returned by the interface is sometimes in the form of a string, and sometimes in the form of a number (you can't trust the back-end classmates too much, you must take precautions yourself ). So add a ' + ' sign in front of it to convert it into a number. Now durationit is converted time!

After we get the duration, we can start the countdown

<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>

A countDown method is created here, which means to start the countdown, and the countdown method will be executed after entering the page.

countDownThe method calls the getTime method, and getTime needs to pass in the duration parameter, which is the remaining time we get.

Now let's implement this method.

<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>

It can be seen that the purpose of getTime is to obtain days, hours, mins, seconds, and then display them on the html, and refresh the values ​​of days, hours, mins, seconds in real time through the timer. Thus realizing the countdown. It's very simple, is there any?

durationFormatterIt is a durationmethod to convert the number of days, hours, minutes, and seconds. It is very simple. You can see its specific implementation.

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 };
},

Well, here comes the problem! !

3. Why use setTimeout to simulate the behavior of setInterval ?

Isn't it more convenient to use setInerval here?

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

You can see what are the disadvantages of setInterval:

Again, the time interval specified by the timer indicates when the timer code is added to the message queue, not when the code is executed. Therefore, the time when the code is actually executed is not guaranteed, it depends on when it is fetched by the event loop of the main thread and executed.

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

As can be seen from the figure above, setInterval adds an event to the queue every 100ms; after 100ms, add the T1 timer code to the queue, and there are still tasks executing in the main thread, so wait, and execute the T1 timer code after some event execution ends; After another 100ms, the T2 timer is added to the queue, and the main thread is still executing the T1 code, so it waits; after another 100ms, theoretically, another timer code needs to be pushed into the queue, but since T2 is still In the queue, so T3 will not be added, and the result is skipped at this time; here we can see that the T2 code is executed immediately after the execution of the T1 timer, so the effect of the timer is not achieved.

In summary, setInterval has two disadvantages:

  1. Some intervals are skipped when using setInterval;
  2. Multiple timers may be executed continuously;

It can be understood as follows: each task generated by setTimeout will be directly pushed to the task queue; and setInterval must make a judgment before pushing the task to the task queue (see whether the last task is still in the queue) .

Therefore, we generally use setTimeout to simulate setInterval to avoid the above shortcomings.

4. Why clearTimeout(this.timer)

Second question: Why is there this.timer && clearTimeout(this.timer);such a sentence?

Assume a scenario:

As shown in the figure, there are two buttons in the parent component of the countdown. Clicking on Activity 1 will pass in the remaining time of Activity 1, and clicking on Activity 2 will pass in the time of Activity 2.

If the countdown component is doing the countdown of activity 1 at this time, and then clicks on activity 2, the new time will be passed in immediately, and the timing needs to be re-timed at this time. Of course, there will be no re-timing here, because the mounted component will only be executed once. That is to say this.countDown();, it will only be executed once, that is to say, this.getTime(this.duration);it will only be executed once, so the duration is still the time of activity one, what should I do? watch came in handy.

Let's monitor the duration. If we find that the duration has changed, it means that the new time time is passed into the component. At this time, we must call this.countDown() again.

code show as below:

<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>

Okay, but it doesn't explain the question raised above: why this.timer && clearTimeout(this.timer);this sentence?

In this way, assuming that the current page displays the time of activity 1, at this time, when setTimeout is executed, the callback function in setTimeout will be put into the task queue after one second , and it will be one second later ! At this time, however, at the beginning of this second, we clicked the Activity 2 button, and the time of Activity 2 will be passed into the countdown component, and then triggered, it will be called, and then executed countDown()to this.getTime(this.duration);setTimeout, it will also be one second Then put the callback function in the task queue.

At this time, there will be two setTimeout callback functions in the task queue. Wait for one second to pass, and the two callback functions are executed one after another, and we will see that the time on the page is reduced by 2 at once. In fact, the operation of subtracting 1 is performed twice very quickly.

this.timer && clearTimeout(this.timer);That's why this sentence was added . It is to clear the previous setTimeout.

5. Using diffTime

When you think this is a perfect component, you want to use this component in the project, assuming you do use it, and it is also online, and you find that there is a big problem: when the page is opened, the countdown starts Yes, the time is 还剩1天12:25:25, and then someone sends you a WeChat message, you switch to WeChat immediately, switch back to the browser after replying to the message, and find that the countdown time is still there 还剩1天12:25:25. You panic: there is a bug in the code you wrote!

How is this going?

For the sake of energy saving, some browsers will suspend scheduled tasks such as setTimeout when they enter the background (or lose focus), and the scheduled tasks will be reactivated when the user returns to the browser

It is said to be suspended, but it should be said to be delayed. The task of 1s is delayed to 2s, and the task of 2s is delayed to 5s. The actual situation varies from browser to browser.

So, it seems that it can’t be as simple as just subtracting 1 every time (after all, setTimeout cools down after you switch the browser to the background, wait a few seconds to switch back, and then execute setTimeout, it’s just a second).

So we need to rewrite the getTime method.

<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>

As you can see, we added new code in three places.

First, add the variable curTime to data, and then assign curTimea value when countDown is executed Date.now(), which is the current moment, which is the moment displayed on the page.

Then look at the modified third code. It can be seen that it will be -1changed -diffTime.

now is the moment when the callback function of setTimeout is executed.

Therefore, diffTime indicates the time period between the execution time of the current setTimeout callback function and the last change of the remaining time on the previous page . In fact, it is the time period between the execution time of the current setTimeout callback function and the execution time of the previous setTimeout callback function.

Maybe you still don't quite understand diffTime. for example:

You opened the countdown page, so you executed countDown, which means you have to execute the getTime method. That is, the following code will be executed immediately.

this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;

After executing these codes, the remaining time will appear on the page.

And this.curTime = Date.now();I recorded the time at this moment.

Then execute the callback function in setTimeout after one second:

const now = Date.now();Record the execution time of the current setTimeout callback function.

const diffTime = Math.floor((now - this.curTime) / 1000);Record the period of time between the execution time of the current setTimeout callback function and the remaining time from the start of rendering on the page. In fact, the diffTime at this time is =1.

Then this.curTime = now;change the value of curTime to the current point in time when the callback function of setTimeout is executed.

this.getTime(duration - diffTime);Actually it isthis.getTime(duration - 1);

Then execute getTime again, the following code will be re-executed, and the new remaining time will be rendered.

this.days = dd || 0;
this.hours = hh || 0;
this.mins = mm || 0;
this.seconds = ss || 0;

Then one second later, the callback function of setTmieout will be executed again. Before the second is over, we will switch the browser to the background, and setTimeout will cool down at this time. Wait 5 seconds before switching back. So the callback function of setTmieout can be executed.

At this time, const now = Date.now();record the execution time of the callback function of the current setTimeout.

And curTime is the execution time of the last setTimeout callback function.

So const diffTime = Math.floor((now - this.curTime) / 1000);actually, the value of diffTime is 5 seconds.

Therefore this.getTime(duration - diffTime);it isthis.getTime(duration - 5);

This perfectly solves the problem that the remaining time remains unchanged because the browser switches to the background.

6. Add a new function: the expiration time can be passed in.

Before, it was only possible to pass in the remaining time, but now it is hoped that it also supports passing in the expiration time.

Just change the duration a bit.

  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;
    }
  },

Determine whether the length of the incoming end is greater than 13 to determine whether it is seconds or milliseconds. easy!

7. Add new function: You can choose what to display, such as only displaying seconds, or only displaying hours.

Just change the 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>

It's very clever. You only need to use the slot to pass the countdown component, that is, the value of the child component to the parent component.

See how the parent component uses this component.

<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>

Look, so ingenious and simple.

I found that 00${hours}the way of writing .slice(-2) is also worth learning. In the past, when the minute was obtained, it was necessary to manually judge whether the obtained minute was a two-digit number or a one-digit number. If it was a one-digit number, it was necessary to manually add 0 in front. Like the following code:

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

And 00${hours}.slice(-2) does not need to judge, first fill in 0, and then intercept two bits from the back to the front.

here.

A perfect countdown component is complete.

3. Study summary

  1. Understand the shortcomings of setInterval and replace setInterval with setTimeout.
  2. Learned “+”, operate, regardless of the three seven twenty-one, convert the long string of numbers obtained by the interface into numbers to keep safe.
  3. Use clearTimeout to clear the previous timer to prevent impact.
  4. Learn to use v-slot to pass value from child to father
  5. Learn a countdown component for the convenience of cv operation in the future. Paste the complete code of the component:
<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>

What have you learned, welcome to add! !

Welcome everyone to leave a message to discuss, I wish you a smooth work and a happy life!

I am the front end of bigo, see you in the next issue.

Guess you like

Origin blog.csdn.net/yeyeye0525/article/details/121741067