【study】Vue如何实现一个倒计时器

6,495 阅读4分钟

1.分析过程

  • 问题:做一个定时器,设定3分钟开始,倒计时到0时,发出警告alert
  • 分析:
  • (1)data存储的数据是不依赖其他数据的,那么什么数据是独立的呢?
  • (2)倒计时的过程中,为什么每秒减一呢?
  • (3)时间是不是在往前流动呢?
  • (4)如果时间不动,那么倒计时是不是就停止了呢?
  • (5)综上所述,根本原因就是时间的流动,所以data里面应该放一个时间戳
  • (6)除了时间戳,我们还应该放倒计时的初始值(设定值),因为在这个例子中,我们的设定值是非响应的,但是在实际的例子中,一定会让用户自己设定倒计时的事件的,所以我们在此例子中先把它放在data里面

2.利用computed+watch+data实现倒计时

<template>
  <div>
    <div>{{ countdownHour }}:{{ countdownMinute }}:{{ countdownSecond }}</div>
  </div>
</template>
<script>
export default {
  data() {
    //初始值设置了2个NaN,因为这两个变量都是数字类型的
    //NaN是数字类型的,但是它表示不是数字,因为我们还没设置他们的值
    return {
      // now是实时的时间,也就是流动的时间
      now: NaN,
      // time是设定的倒计时,如3分钟,换算成和timestamp一样的单位,就是3 * 60e3 = 180000
      // 需要自己定义倒计时时间
      time:6000,
    };
  },
  created() {
    // 先定义一个函数update,即每次更新的函数
    // update做了一件事,就是会设置一个定时器,并在1秒钟后递归调用自身,并且将时间更新
    
    //第一种方法:用setTimeout
    // const update = () => {
    //   setTimeout(() => {
    //     // 因为递归调用自身,所以又设置了另外一个延迟1秒的操作
    //     update();
    //     // 当然,这样写有个问题,就是第1秒的时候是now还是NaN
    //     // 因为要1秒后才会更新this.now
    //     // 所以在第一秒可以手动设置this.now = Date.now()
    //     this.now = Date.now();
    //   }, 1e3);
    // };
    // update();
    
    //第二种方法:用requeatAnimationFrame
    const update = () => {
      // 也是循环调用自身,这次会在一开始就更新this.now
      this.now = Date.now();
      // requestAnimationFrame接受一个参数,这个参数是一个函数
      // 因为在update里调用requestAnimationFrame(update),相当于设定下次屏幕刷新前执行一下update
      // 那么下次执行update的时候,又会设置下一帧要调用update
      //更新的事件由API把握
      //requestAnimationFrame在下次屏幕刷新前,所以不同的显示器,刷新率不一样,调用的次数也不同,但是,能保证,每次重绘的时候总是最新的时间
      requestAnimationFrame(update);
    };
    update();

    //这里面不管是用了setTimeout还是requestAnimationFrame,都使用了箭头函数,因为this.now中的this需要指向组件实例
  },
  computed: {
    // 结束时间
    finishTime() {
      //这里并没有用this.now,因为this.now记录的是实时时间
      //这里的是设定的时间+设定的倒计时毫秒数
      return Date.now() + this.time;
      //this.time的变化,触发这个结束的时间,只要this.time不更新,结束时间就不会重新计算,所以结束时间一直固定
    },
    //剩余的毫秒数
    countdown() {
      //倒计时不会小于0,也就是说countdown最小值是0
      return Math.max(0, this.finishTime - this.now);
    },
    // 时
    countdownHour() {
      //padStart是用于补位
      return String.prototype.padStart.call(
        (this.countdown / 3.6e6) | 0,
        2,
        "0"
      );
    },
    // 分
    countdownMinute() {
      return String.prototype.padStart.call(
        ((this.countdown % 3.6e6) / 6e4) | 0,
        2,
        "0"
      );
    },
    // 秒
    countdownSecond() {
      return String.prototype.padStart.call(
        ((this.countdown % 6e4) / 1e3) | 0,
        2,
        "0"
      );
 },
  },
  // 监听时间到了:
   watch: {
      countdown(countdown) {
         if (0 === countdown) {
           alert("时间到了");
       }
     },
   },
};
</script>

3.利用computed+watch+data+filter实现倒计时

<template>
  <!-- 2控制的是显示2位,就是过滤器里面的length -->
  <div>{{ countdown | display(2) }}</div>
</template>
<script>
export default {
  data() {
    return {
      now: NaN,
      time: 12000,
    };
  },
  filters: {
    //定义display过滤器,第一个value是输入值,length是过滤器的选项
    display(value, length) {
      const hour   = (value / 3.6e6) | 0,
            minute = ((value % 3.6e6) / 6e4) | 0,
            second = ((value % 6e4) / 1e3) | 0;
      //用length控制补0后的长度
      const format = (number) =>
        String.prototype.padStart.call(number, length, "0");

      return `${format(hour)}:${format(minute)}:${format(second)}`;
    },
  },

  created() {
    const update = () => {
      this.now = Date.now();
      requestAnimationFrame(update);
    };
    update();
  },
  computed: {
    // 结束时间
    finishTime() {
      return Date.now() + this.time;
    },
    //剩余的毫秒数
    countdown() {
      return Math.max(0, this.finishTime - this.now);
    },
  },

  watch: {
    countdown(countdown) {
      if (0 === countdown) {
        alert("时间到了");
      }
    },
  },
};
</script>

4.在3的基础上,可以继续串联filter进行过滤,把:变成-

<template>
  <!-- 2控制的是显示2位,就是过滤器里面的length -->
  <div>{{ countdown | display(2)|transform(":", "-") }}</div>
</template>
<script>
export default {
  data() {
    return {
      now: NaN,
      time: 12000,
    };
  },
  filters: {
    //定义display过滤器,第一个value是输入值,length是过滤器的选项
    display(value, length) {
      const hour   = (value / 3.6e6) | 0,
            minute = ((value % 3.6e6) / 6e4) | 0,
            second = ((value % 6e4) / 1e3) | 0;
      //用length控制补0后的长度
      const format = (number) =>
        String.prototype.padStart.call(number, length, "0");

      return `${format(hour)}:${format(minute)}:${format(second)}`;
    },
    //pattern是要替换的部分,char是要替换成什么
     transform(value, pattern, char) {
      return value.replace(new RegExp(pattern, "g"), char);
    },

  },

  created() {
    const update = () => {
      this.now = Date.now();
      requestAnimationFrame(update);
    };
    update();
  },
  computed: {
    // 结束时间
    finishTime() {
      return Date.now() + this.time;
    },
    //剩余的毫秒数
    countdown() {
      return Math.max(0, this.finishTime - this.now);
    },
  },

  watch: {
    countdown(countdown) {
      if (0 === countdown) {
        alert("时间到了");
      }
    },
  },
};
</script>

5.使用computed和filter的差异(上面2和3方法的差异)

  • (5.1) 从上述写法我们可以看到,countdown这个属性是一直在变的,因为now一直在变,而且它变得非常快,不需要1秒那么长才变一次,在1秒内会变动多次,但实际上,我们现实的最大精度到秒就可以了
  • (5.2)this.now同步Date.now,所以其实countdown的频繁更新不会体现到视图上,因为countdown在1秒内的变化,视图上其实是没有任何变化的
  • (5.3)filter:
  • (1)因为template里直接用了countdown,所以只要countdown一变,就会马上重新渲染,这里的重新渲染是指重新生成虚拟DOM
  • (2)就是会调用display过滤器,所以如果在display里加了console的话,会发现,这个过滤器被调用得非常频繁
  • (3)最终视图没更新,是因为经过过滤器之后,1秒的变化其实没有造成任何变化,过滤器抹平了差异
  • (4)最终,虚拟DOM经过对比后发现,不需要重新渲染真实的DOM,才没更新
  • (5.4)computed:
  • (1)countdown一直在变,所以会造成时分秒这3个属性不断地被计算
  • (2)但是时分秒计算后,适合分就变得不那么快了,秒也是这样,只有1秒才变一次
  • (3)所以这个时候不会频繁的进行虚拟DOM的重新渲染,只有当变化最快的,也就是秒变化时,才会重新渲染一次
  • (4)和filter对比,其更新频率下降了很多,因为减少了虚拟DOM的渲染次数,就减少了虚拟DOM的比较次数,虚拟DOM的新旧比较diff操作也是非常耗资源的
  • (5.5)所以在比较虚拟DOM渲染的次数上,更新频率上,使用computed更好一些
  • (5.6)既然这样,那么为什么filter比computed好呢?
  • 问题在于,我们要显示的是秒,但是变动频繁的是毫秒,是因为这个原因才导致的,如果我们这时候要显示毫秒精度的倒计时,那么肯定filter更好,因为不需要computed这些属性做缓存了
  • 总结:
  • (1)如果要给数据做变形,如格式化,那么就应该用filter,不要在造出额外的数据了,filter纯粹是为了显示而存在的
  • (2)如果要对数据做加工,加工后的数据不是用来显示,而是可能会用在某些地方(可能有些条件,能用或者用不到),那么这是要用computed

6.上述的例子中,有没有办法既用filter又让filter比computed有用呢?

  • 看第5点的分析可知,是因为单位不统一造成的,所以我们可以把now改成单位为秒的数值,相应的,其他有关的计算都要改一下
  • 而在update这个函数中,我们每次赋值的时候只赋值单位为秒的当前时间,换句话说,2次赋值可能是同一个值,这个没关系,因为set劫持器会对数据比较,如果相同,根本不会触发更新
  • 通过这种方式,让filter又重新比computed好用了
<template>
  <!-- 2控制的是显示2位,就是过滤器里面的length -->
  <div>{{ countdown | display(2) }}</div>
</template>
<script>
export default {
  data() {
    return {
      now: NaN,
      time: 60,
    };
  },
   filters: {
    display(value, length) {
      const hour = (value / 3.6e3) | 0,
        minute = ((value % 3.6e3) / 6e1) | 0,
        second = ((value % 6e1) / 1e0) | 0;

      const format = (number) =>
        String.prototype.padStart.call(number, length, "0");

        // console.log(`${format(hour)}:${format(minute)}:${format(second)}`)
      return `${format(hour)}:${format(minute)}:${format(second)}`;
     
    },
  },

  created() {
    const update = () => {
      this.now = (Date.now() / 1e3 | 0);
      requestAnimationFrame(update);

    };
    update();
  },
  computed: {
    // 结束时间
    finishTime() {
      return (Date.now()/1e3 | 0) + this.time;
    },
    //剩余的毫秒数
    countdown() {
      return Math.max(0, this.finishTime - this.now);
    },
  },

  watch: {
    countdown(countdown) {
      if (0 === countdown) {
        alert("时间到了");
      }
    },
  },
};
</script>