One, foreword

My first requirement was to complete an activity project with a front-end boss.

Since we are developing together, we 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 took it directly to use.

It feels great to pass a parameter and realize the function. After the project was completed, I worshipped the code of the big guy’s countdown component. I really learned a lot. The list is as follows:

  1. Why use setTimeout instead of setInterval for timers
  2. Why don’t you just take the remaining time minus 1.
  3. How to return the required time (maybe I only need minutes and seconds, just minutes and seconds, or maybe I want all).
  4. It is not certain whether the interface returns the remaining time or the expiration date, and how to accommodate both.
  5. Not sure if the time returned by the interface is in seconds or milliseconds.

Ok, you may not understand these questions very well, but that’s ok, after reading the explanation below, I believe you will be enlightened.

Two, start hand exercises

1. Create a VUE component

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

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

</style>
Copy the code

2. Implement basic countdown components

Next, assume that the interface gets a residual time.

Time (millisecond) {return time (millisecond) {return time (millisecond) {return time (millisecond); As shown in the following code.

<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);
      returntime; }}};</script>
<style lang='scss' scoped>

</style>
Copy the code

Duration in computed is the result of converting time, whether milliseconds or seconds, to seconds: +this.time. Why put a ‘+’ in front of it. This is worth learning, because the interface returns a string of numbers sometimes as a string, sometimes as a number (don’t trust the backend students too much, you have to take precautions). So by putting a ‘+’ in front of it, it all turns into numbers. Duration is now converted to time.

Once we get the Duration, we can start counting down

<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);
      returntime; }},// Add code:
  mounted() {
    this.countDown();
  },
  methods: {
    countDown() {
      this.getTime(this.duration); }}};</script>
<style lang='scss' scoped>

</style>
Copy the code

A countDown method is created to countDown as soon as the page is entered.

The countDown method calls the getTime method, which takes the duration argument, which is the remaining time we get.

Now let’s implement this method.

<template>
  <div class="_base-count-down">{{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);
      returntime; }},mounted() {
    this.countDown();
  },
  methods: {
    countDown() {
      this.getTime(this.duration);
    },
    // Add code:
    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>
Copy the code

As you can see, the purpose of getTime is to get days, hours,mins, and seconds, display them in HTML, and refresh the days, hours,mins, and seconds values in real time through the timer. The countdown is realized. It’s easy, isn’t it?

DurationFormatter is a method that converts duration to days, hours, minutes, and seconds.

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 };
},
Copy the code

Ok, here comes the problem!!

3. WhyUse setTimeout to simulate the behavior of setInterval?

Isn’t it easier to use setInerval here?

SetTimeout (function () {...}, n); // execute function after n millisecondsCopy the code
SetInterval (function () {...}, n); // Execute function every n millisecondsCopy the code

Take a look at the drawbacks 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. So the actual time when the code is executed is not guaranteed, depending on when it is picked up by the main thread’s event loop and executed.

SetInterval (function, N) // that is, function events are pushed to the message queue every N secondsCopy the code

As you can see in the figure above, setInterval adds an event to the queue every 100ms. 100ms later, add T1 timer code to queue, there are still tasks in the main thread, so wait, some event is finished to execute T1 timer code; After another 100ms, the T2 timer was added to the queue. The main thread was still executing T1 code, so it waited. After another 100ms, another timer code would theoretically be pushed into the queue, but since T2 was still in the queue, T3 would not be added and would be skipped. Here we can see that the T2 code was executed immediately after the T1 timer had finished, so the effect of the timer was not achieved.

To sum up, setInterval has two drawbacks:

  1. When using setInterval, certain intervals are skipped;
  2. Multiple timers may run consecutively.

The task generated by each setTimeout is pushed directly into the task queue. SetInterval does a check (to see if the last task is still in the queue) every time it pushes a task to the queue.

Therefore, setTimeout is generally used to simulate setInterval to avoid the above shortcomings.

4. 为什么要clearTimeout(this.timer)

This. Timer && clearTimeout(this.timer); This sentence?

Consider 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 counting down to activity 1, then click on activity 2, a new time will be passed immediately, at which point it will need to be retimed. Of course, there is no re-timing, because component Mounted is executed only once. This.countdown (); This is executed only once, that is, this.getTime(this.duration); Duration is executed only once, so duration is the duration of activity one. Watch comes in handy.

If duration changes, a new time component is passed in, and this.countdown () will be called again.

The code is as follows:

<template>
  <div class="_base-count-down">{{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);
      returntime; }},mounted() {
    this.countDown();
  },
  // Add code:
  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>
Copy the code

Ok, but that doesn’t explain the question raised above: why this.timer && clearTimeout(this.timer); This sentence?

So, assuming that the page now displays the time of activity 1, when setTimeout is executed, the callback function in setTimeout will be placed in the task queue after one second. At this point, however, at the beginning of the second, we click the activity 2 button, and the time of activity 2 is passed into the countDown component, which triggers countDown(), which calls this.getTime(this.duration); , and setTimeout, which also puts the callback function on the task queue one second later.

There will be two setTimeout callbacks in the task queue. We wait a second for the two callbacks to be executed, and we see the time on the page decrement by 2, actually decrement by 1 twice very quickly.

This is why this. Timer && clearTimeout(this.timer) is added; This sentence is the reason. I want to get rid of the previous setTimeout.

5. Use diffTime

Just when you think this is a perfect component and you want to use it on your project, assuming you do use it and get it online, there is a big problem: when the page opens, the countdown starts and there is one day left at 12:25: 25, then someone sends you a message on wechat. You immediately switch to wechat, reply to the message and check back to your browser, only to find that the countdown time still has one day left at 12:25:25. You panic: your code is buggy!

What’s going on here?

To save energy, some browsers will suspend scheduled tasks such as setTimeout when entering the background (or losing the focus) and reactivate scheduled tasks when the user returns to the browser

When I say pause, I should say delay. The 1s task is delayed to 2s, and the 2s task is delayed to 5s, depending on the browser.

After all, when you cut the browser to the background, setTimeout cools down, wait a few seconds, cut back, and then execute setTimeout, which is just one second less.

So we need to rewrite the getTime method.

<template>
  <div class="_base-count-down">{{hours}}:{{mins}}:{{seconds}}</div>
</template>
<script>

export default {
  data: () = > ({
    days: '0'.hours: '00'.mins: '00'.seconds: '00'.timer: null.curTime: 0.// Add code:
  }),
  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);
      returntime; }},mounted() {
    this.countDown();
  },
  
  watch: {
    duration() {
      this.countDown(); }},methods: {
    countDown() {
      // Add code:
      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(() = > {
        // Add code:
        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>
Copy the code

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

We first add the variable curTime to data and, upon countDown, assign the value date.now () to curTime, which is the current moment, as shown on the page.

Then look at the third code change. You can see that -1 is changed to -difftime.

Now is the moment when the setTimeout callback is executed.

DiffTime therefore represents the time period between the time when the current setTimeout callback was executed and the last change in the remaining time on the page. In fact, this is the time range between the current setTimeout callback and the previous setTimeout callback.

DiffTime may still be confusing to you. Here’s an example:

You open the countDown page and countDown is executed, which means the getTime method is executed. That is, the following code is immediately executed.

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

The remaining time will appear on the page after executing this code.

While this. CurTime = Date. Now (); I recorded the exact moment in time.

Then execute the setTimeout callback one second later:

const now = Date.now(); Records the point in time at which the setTimeout callback is executed.

const diffTime = Math.floor((now – this.curTime) / 1000); Records the time between the current setTimeout callback and the rest of the rendering time on the page. In fact, diffTime in this case is equal to 1.

Then this.curTime = now; Change the value of curTime to the point at which the current setTimeout callback is executed.

this.getTime(duration – diffTime); This.gettime (duration-1);

Then getTime is executed again, and the code below is re-executed, with the new remaining time rendered.

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

A second later, the setTmieout callback is executed. Before the second ends, we cut the browser to the background and setTimeout cools down. Wait five seconds before cutting back. The setTmieout callback is then executed.

Const now = date.now (); Records the point in time at which the setTimeout callback is executed.

CurTime is the time at which the previous setTimeout callback was executed.

So const diffTime = math.floor ((now-this.curtime) / 1000); In fact, the diffTime value is 5 seconds.

Thus this.getTime(duration-diffTime); This.gettime (duration-5);

This perfectly solves the problem of remaining the same time because the browser cuts to the background.

6. Added new feature: You can pass in expiration time.

Previously you could only pass in the remaining time, now you want to support passing in the expiration time as well.

Just change 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);
      returntime; }},Copy the code

Determine whether the length of the passed end is greater than 13 seconds or milliseconds. Easy!

7. Add new features: you can choose what to display, such as seconds only, or hours only.

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>
Copy the code

It’s very clever, isn’t it, just by using the slot, you’re passing the countdown component, which is passing the value of the child component to the parent component.

See how the parent uses this component.

<base-counter v-slot="timeObj" :time="countDown"> <div class="count-down"> <div class="icon"></div> Day. {{timeObj d}} {{timeObj. Hh}} hour is {{timeObj. Mm}} {{timeObj. Ss}} seconds < / div > < / a > base - counterCopy the code

Look, it’s so clever and simple.

00${hours}. Slice (-2) is also worth learning. In the past, when the minutes were obtained, you had to manually determine whether the minutes were two digits or one digit. If it was one digit, you had to manually fill in 0 in front of it. Like the following code:

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

while00${hours}For slice(-2), add 0 first, and then slice two digits from back to front.

To this.

A perfect countdown component is complete.

Three, learning summary

  1. Understand the disadvantages of setInterval and use setTimeout instead of setInterval.
  2. learned"+", operation, no matter 37, 21, the interface to get a long string of numbers into digital security.
  3. Use clearTimeout to clear previous timers to prevent impact.
  4. Learn to use V-slot to pass values from child to parent
  5. Learn a countdown component to facilitate CV operation in the future. Paste the complete component code:

The last

The public account “Front-end Sunshine”, reply to add group, welcome to join the technical exchange group and internal push group.

<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);
      returntime; }},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; // The page will not be timed when it returns to the background
        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>
Copy the code