秒杀倒计时功能的实现

2,759 阅读5分钟

使用定时器获取倒计时

我们项目是使用的vue框架,所以我们在开始写业务逻辑之前,可以先封装一个将倒计时拆分为时分秒的工具函数, 方便后期复用。
代码如下:

// util.js
/**
 * 获取时间对象
 * @param(Number) 剩余倒计时的时间戳
 * @param(Boolean) 是否限制小时在24小时以内
*/
export const getDateObj = (times, isLimitHour) => {
  const second = times / 1000
  const leftSecond = ~~(second % 60)
  const leftMinutes = ~~(second / 60 % 60)
  const leftHours = isLimitHour ? ~~(second / 60 / 60) : ~~(second / 60 / 60 % 24)
  const leftDay = ~~(second / 60 / 60 / 24)
  return {
    day: leftDay,
    hours: leftHours,
    minutes: leftMinutes,
    second: leftSecond
  }
}

工具函数实现后,我们就可以在组件中引入该函数,并根据业务需求来使用实现倒计时功能。 下边是简洁版的倒计时的实现(只有倒计时逻辑,无视图代码)

import { getDateObj } from '@/utils/util';
export default {
  data() {
    return {
      timer: null,
      times: 1122404, // 剩余时间(毫秒)
      countDown: ''
    }
  },
  mounted() {
    this.setCountDown();
  },
  methods: {
    // 设置倒计时
    setCountDown() {
      clearInterval(this.timer);
      this.timer = setInterver(() => {
        this.times -= 1000;
        let { hours, mintues, second } = getDateObj(this.times);
        this.countDown = hours + ':' + mintues + ':' + second;
        if (!hours && !mintues && !second) { // 倒计时结束时
          clearInterval(this.timer);
          // 执行其他特殊操作
        }

      }, 1000);
    }
  }
}

使用一个定时器显示多个倒计时

有时一个页面可能会有多个倒计时的显示,此时如果一个倒计时对应一个定时器,不仅会增大浏览器的资源消耗,定时器过多还容易造成倒计时的混乱,在页面多次刷新等情形下倒计时会变得非常快,同时在页面销毁时清除定时器也比较麻烦。此时我们就可以只用一个定时器实现多个倒计时。

import { getDateObj } from '@/utils/util';
export default {
  data() {
    return {
      timer: null,
      list: [
        {
          name: '倒计时1',
          endTime: 123456000,
          countDown: 0
        },
        {
          name: '倒计时2',
          endTime: 126456000,
          countDown: 0
        }
      ]
    }
  },
  mounted() {
    this.setCountDown();
  },
  methods: {
    // 设置倒计时
    setCountDown() {
      clearInterval(this.timer);
      this.timer = setInterver(() => {
        this.times -= 1000;
        this.list = this.list.map(item => {
          let { hours, mintues, second } = getDateObj(this.times);
          item.countDown = hours + ':' + mintues + ':' + second;
          if (!hours && !mintues && !second) { // 倒计时结束时
            clearInterval(this.timer);
            // 执行其他特殊操作
          }
          return item;
        });
      }, 1000);
    }
  }
}

使用web worker定义倒计时

我们知道setInterval是异步的,这就会产生一个问题,当有事件发生阻塞时,setInterval就会被卡住,倒计时就会变慢。
此时我们就可以用到 web Worker来创建一个子线程,将定时器放入子线程中执行,这样就不会受到主线程的影响而发生阻塞 或拖慢。

web worker的详细介绍,可以查看阮一峰老师的文章传送门

  1. 首先我们先新建一个Wirker线程 一般情况下,子线程执行的代码需要写在一个单独的js中。如下所示
var worker = new Worker('work.js')

不过,我们也可以使用blob + URL.createObjectURL来创建一个虚拟的文件,之后再执行web worker。

function getWorker(worker, param) {
  const code = woker.toString();
  const blob = new Blob([`(${code})(${JSON.stringify(param)})`]);
  return new Worker(URL.createObjectURL(blob));
}
  1. 子线程内实现倒计时 步骤如下:
  • 监听主线程消息,开始和结束对应的计时时间
  • 处理倒计时得到的天,时,分,秒的信息
  • 每过一秒发送一次对应的信息给主线程
/**
 * 生成子线程的倒计时
 * @param {*} param 
 */
export function setWorkCountDown(param) {
  let _timer = null
  let { endTime } = param
  this.onmessage = e => {
    const { data: { type, interval } } = e

    const countDown = (milsecond) => {
      const second = milsecond / 1000

      if (milsecond < 0) {
        clearInterval(_timer)
        return {
          day: 0,
          hours: 0,
          minutes: 0,
          second: 0
        }
      }

      const leftSecond = ~~(second % 60)
      const leftMinutes = ~~(second / 60 % 60)
      // const leftHours = ~~(second / 60 / 60 % 24)
      const leftHours = ~~(second / 60 / 60)
      const leftDay = ~~(second / 60 / 60 / 24)

      // return [leftDay, leftHours, leftMinutes, leftSecond]
      return {
        day: leftDay,
        hours: leftHours,
        minutes: leftMinutes,
        second: leftSecond
      }
    }

    nextTick = countDown(endTime)

    if (type === 'start') {
      _timer = setInterval(() => {
        // 通过postMessage告诉父进程 我已经计时了1秒了 你可以操作了
        this.postMessage({ nextTick })
        endTime = endTime - interval
        nextTick = countDown(endTime)
      }, interval)
    } else if (type === 'end') {
      clearInterval(_timer)
    }
  }
}
  1. 主线程启动倒计时
    步骤如下:
    • 启动子线程
    • 监听子进程倒计时时间
const worker = getWorker(setWorkCountDown, { endTime: 690200000 });
worker.postMessage({ type: 'start', 1000});
  1. 关闭子线程的倒计时并释放子线程的内存
worker.postMessage({ type: 'end'}); // 关闭子线程的定时器
worker.terminate(); // 释放子线程的内存

为了更好复用,在vue中,我们可以将它封装在工具函数,方便我们在项目中使用它。
完整代码如下:

// util.js
/**
 * 动态生成一个web worker文件
 * @param {*} worker 
 * @param {*} param 
 */
export function getWorker(worker, param) {
  const code = worker.toString()
  const blob = new Blob([`(${code})(${JSON.stringify(param)})`])
  return new Worker(URL.createObjectURL(blob))
}

/**
 * 生成子进程的倒计时
 * @param {*} param 
 */
export function setWorkCountDown(param) {
  let _timer = null
  let { endTime } = param

  this.onmessage = e => {
    const { data: { type, interval } } = e

    const countDown = (milsecond) => {
      const second = milsecond / 1000

      if (milsecond < 0) {
        clearInterval(_timer)
        return {
          day: 0,
          hours: 0,
          minutes: 0,
          second: 0
        }
      }

      const leftSecond = ~~(second % 60)
      const leftMinutes = ~~(second / 60 % 60)
      // const leftHours = ~~(second / 60 / 60 % 24)
      const leftHours = ~~(second / 60 / 60)
      const leftDay = ~~(second / 60 / 60 / 24)

      // return [leftDay, leftHours, leftMinutes, leftSecond]
      return {
        day: leftDay,
        hours: leftHours,
        minutes: leftMinutes,
        second: leftSecond
      }
    }

    nextTick = countDown(endTime)

    if (type === 'start') {
      _timer = setInterval(() => {
        // 通过postMessage告诉父进程 我已经计时了1秒了 你可以操作了
        this.postMessage({ nextTick })
        endTime = endTime - interval
        nextTick = countDown(endTime)
      }, interval)
    } else if (type === 'end') {
      clearInterval(_timer)
    }
  }
}

/**
 * 得到子进程的倒计时对象
 * @param {*} times 倒计时时间
 * @param {*} interval 间隔
 */
export function getWorkerCountDownObj(times, interval = 1000) {
  const worker = getWorker(setWorkCountDown, { endTime: times });
  worker.postMessage({ type: 'start', interval});
  return worker;
}
import { getWorkerCountDownObj } from '@/utils/util';
export default {
  data() {
    return {
      times: 1122404, // 剩余时间(毫秒)
      countDown: '',
      worker: null // web worker对象
    }
  },
  mounted() {
    this.setCountDown();
  },
  methods: {
    // 设置倒计时
    setCountDown() {
      // 获取worker对象
      this.worker = getWorkerCountDownObj(this.times);
      this.worker.onmessage = (e) => {
        // 获取子进程中的倒计时的时分秒
        let { hours, minutes, second } = e.data.nextTick;
        this.countDown = hours + ':' + mintues + ':' + second;
        if (!hours && !mintues && !second) { // 倒计时结束时
          this.worker.postMessage({ type: 'end'}); // 关闭子进程的定时器
          this.worker.terminate(); // 释放子进程的内存
          // 执行其他特殊操作
        }
      }
    }
  }
}

倒计时其他注意点

  1. pc端多页面切换,手机端锁屏,应用进入后台时定时器停滞或变慢问题的解决。
  • 1.1 普通h5应用解决方法 使用document对象上新增document.visibilityState属性来判断页面的可见性,根据页面可见性重新校准时间。
export default {
  mounted() {
    // 监听页面可见性事件
    document.addEventListener('visibilitychange', this.visibilitychange, false);
  },
  destroyed() {
    document.removeEventListener('visibilitychange', this.visibilitychange);
  },
  methods: {
    visibilitychange() {
      // 用户离开了当前页面
      if (document.visibilityState === 'hidden') {
        clearIntervar(this.timer);
      }
      // 用户打开或回到页面
      if (document.visibilityState === 'visible') {
        clearIntervar(this.timer);
        this.setCountDown(); 
      }
    },
    // 倒计时方法
    setCountDown() {}
  }
}

document.visibilityState的详细介绍和使用可以看阮一峰老师的一篇文章传送门

  • 1.2 小程序内嵌网页(webview)
    在小程序内嵌网页(webview)应用中使用document.visibilityState属性无法监听手机端锁屏,此时我们可以使用 微信提供的API来监听页面的是否在前台.
export default {
  mounted() {
    // 监听页面是否在前台
    WeixinJSBridge.on('onPageStateChange', this.visibilitychange);
  },
  methods: {
    visibilitychange() {
      if (res.active) {
        this.setCountDown(); 
      } else {
        clearInterval(this.timer);
      }
    },
    // 倒计时方法
    setCountDown() {}
  }
}

详细介绍请看微信小程序官方文档