前端如何实现一个倒计时组件?

简介: 前端如何实现一个倒计时组件?

需求


倒计时这种需求非常常见。在我接触的项目中,已经做过N个倒计时的需求。常见的场景有电商项目中的秒杀抢购活动倒计时,短信验证码等。

现在再一次碰到了倒计时的需求,是一个答题倒计时的场景。具体效果如图。


思路


要实现倒计时,就需要来回顾一下JavaScript中的定时器相关知识。

  1. setInterval: 每间隔N秒执行一次回调函数。
  2. setTimeout: N秒后执行回调函数。
  3. setImmediate: 非标准的API,目前尚未被正式采纳,用于执行耗时的运算。执行完其它代码,就会立即执行。
  4. requestAnimationFrame: 类似于setInterval,用于动画。采用系统时间,保证时间的准确性,但无法指定间隔时间。

从上面可以看出,能指定某个时间触发的定时器,仅有setIntervalsetTimeout,这种场景下,我选择setInterval。当然也可以对setTimeout进行递归,不断重新创建和销毁的方式,达到同样的效果。

首先我们明白,因为JavaScript是单线程的,在事件循环过程中,当前宏观任务队列中的微观任务会阻塞下一个宏观任务队列中任务的执行。所以会造成一种现象,定时器中的真实执行时间并不会精准的按照第2个参数所设定的数值执行。比如设置1000毫秒,如果到了1000毫秒,主线程被其他任务所占用了,那么就会等待其它任务的执行,等其它任务执行完毕后,才会执行定时器的回调函数。

也就是说,如下代码代表的意思不是1秒后执行,而是最快1秒后执行。


setTimeout(() => {console.log('我是定时器!')},1000);


你可以尝试执行如下代码,会发现定时器的执行时间应该超过了1秒钟,如果正常执行,你可以从循环条件后面加个0。电脑配置很差的就不要试了。


setTimeout(() => {console.log('我是定时器!')}, 1000);
for (let i = 0; i<1000000000; i++) {}


碰到这种循环或者递归代码时,回调函数的执行时间会根据不同的电脑运算速度决定。如果你的电脑配置够强,比如小型机,高性能服务器等,能够在1秒以内执行完逻辑,那么就不会影响定时器的正常执行。

要想做到时间相对准确,就必须解决这个问题,办法有很多种,最常见也最有效的办法,是在当前定时器的回调函数中校验误差并调整下一次定时器的发生时间,达到平均1秒的效果。

掘金上面有一篇介绍这种做法的文章,可供参考:juejin.cn/post/684490…

但是,如果在浏览器中单独打开一个空白页面,在控制台中运行如下代码,观察每次的输出,发现还是足够准确的,误差都在1毫秒以内。


setInterval(() => { console.log(new Date().getTime()); }, 1000);


https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/9/30/16d826fa4ba7494e~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image

这是不是就意味着我们可以直接这么写代码呢?如果页面足够简单,没有其它的监听事件,不会发生频繁的交互操作,这么写仍然会出问题,当页面休眠时,定时器就会停止。如果页面存在很多监听事件或者交互操作,就可能会发生跳秒的现象。特别是在单页面应用中更应该注意,像reactvue框架中,diff算法和DOM渲染都在一个主线程中执行。

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/9/30/16d82776ed81c915~tplv-t2oaga2asx-zoom-in-crop-mark:4536:0:0:0.image

为了最大程度的避免这个问题,可以采用web worker来开启一个后台线程单独运行定时器,但是这样也只是能够保证计时器的运行间隔是精准的,并不能保证UI渲染是精准的。

目前web worker支持度已经非常好了,基本上不需要担心兼容性问题。


使用web worker的唯一方式就是通过new Worker('../xx.js')的方式使用。构造参数是独立线程js文件的路径。在react框架中,只能引用public目录下的文件,才能保证打包后路径是正确的。或者修改webpack配置,但这样做并不是很优雅。

虽然使用web worker的方式只有一种,但是我们可以在遵循正常使用规则下,用一种更优雅的方式来实现。通过Blob对象和URL.createObjectURL方法来创建一个虚拟的js文件。

具体实现代码如下:


// worker.js
export default class WebWorker {
  constructor(worker) {
    const code = worker.toString();
    const blob = new Blob([`(${code})()`]);
    return new Worker(URL.createObjectURL(blob));
  }
}


这个类接受一个构造参数,这个构造参数是一个函数,通过Blob创建这个虚拟的js文件。再通过URL.createObjectURL方法为Blob对象创建一个链接。最终作为Worker的构造参数,来创建一个worker实例。

使用它也比较简单。


// CountDownTimer.jsx
import WebWorker from "../../utils/worker";
let work = function() {
  let timer = null;
  this.onmessage = e => {
    const { endTime, state } = e.data;
    if (state === "stop") {
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
      return;
    } else if (state === "start") {
      let interval = 1000;
      if (!timer) {
        timer = setInterval(() => {
          this.postMessage(endTime - new Date().getTime());
        }, interval);
      }
    }
  };
};
let worker = new WebWorker(work);

这样就可以发送给worker一个开始指令。


worker.postMessage({
      state: "start",
      endTime: Number.parseInt(endTime, 10)
    });

然后监听worker的响应。


worker.onmessage = e => {
    if (e.data <= 0) {
      worker.postMessage({ state: "stop" });
      return;
    }
    setTime(relativeTime(e.data));
  };


这里仍然是一个无法解决的问题。由于DOM的绘制是在主线程内完成的,web worker不能处理DOM,虽然可以保证定时器的间隔精准度,但无法保证主线程更新UI的精准度。如果主线程在处理其它事情,onmessage不能及时响应,UI仍然会发生卡顿。

所以,文章写到这,关于定时器相关的知识差不多就讲完了。最后你肯定还有个问题没明白,既然onmessage也会被阻塞,也会导致UI更新不及时,那和直接在主线程中写setInterval又有什么区别呢?为什么要这么麻烦的写到web worker中?

在看答案之前,你不妨先思考一下。

最大的区别在于:setInterval在被阻塞一次后,后面的所有执行时间间隔都会被打乱,如果被阻塞N次,时间间隔就会越来越乱。web worker的作用就是即使被阻塞N次,也能保证定时器中的函数执行次数是按照预期执行的。

为了避免这种情况可以按照上面提到的那种不断进行时间纠偏、重新创建setTimeout的方式来实现。web worker的方式是一种新的实现思路,其优势在于无论主线程如何阻塞,定时器的回调函数执行次数和频率是不会受到影响的。


具体实现


组件的用法:


<CountDownTimer endTime={1569834068266} onEnd={this.onEndHandler.bind(this)} />

组件接口设计如下:

  1. endTime 结束的时间戳,13位字符串
  2. onEnd 到达结束时间所要执行的回调函数

组件代码

CountDownTimer.jsx


import React from "react";
import styled from "styled-components";
import WebWorker from "../../utils/worker";
let work = function() {
  let timer = null;
  this.onmessage = e => {
    const { endTime, state } = e.data;
    if (state === "stop") {
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
      return;
    } else if (state === "start") {
      let interval = 1000;
      if (!timer) {
        timer = setInterval(() => {
          this.postMessage(endTime - new Date().getTime());
        }, interval);
      }
    }
  };
};
let worker = new WebWorker(work);
/**
 * 计算相对时间字符串
 * @param {number} time 13位时间戳
 */
function relativeTime(time) {
  if (time <= 0) {
    return "00:00";
  }
  const minute = Number.parseInt(time / 1000 / 60, 10);
  const second = Number.parseInt((time / 1000) % 60, 10);
  return `${minute > 9 ? minute : "0" + minute}:${
    second > 9 ? second : "0" + second
  }`;
}
export default function CountDownTimer({
  endTime,
  onEnd = Function.prototype
}) {
  const initTime = relativeTime(endTime - new Date().getTime());
  let [time, setTime] = React.useState(initTime);
  worker.onmessage = e => {
    if (e.data <= 0) {
      worker.postMessage({ state: "stop" });
      return;
    }
    setTime(relativeTime(e.data));
  };
  React.useEffect(() => {
    worker.postMessage({
      state: "start",
      endTime: Number.parseInt(endTime, 10)
    });
    return function() {
      worker.postMessage({ state: "stop" });
    };
  }, [endTime]);
  React.useEffect(() => {
    if (time === "00:00") {
      return function() {
        onEnd();
        worker.postMessage({ state: "stop" });
      };
    }
  }, [time, endTime, onEnd]);
  const Time = styled.span`
    font-size: 1.6rem;
    font-weight: 700;
    vertical-align: middle;
  `;
  return (
    <div>
      倒计时:<Time>{time}</Time>
    </div>
  );
}


export default class WebWorker {
  constructor(worker) {
    const code = worker.toString();
    const blob = new Blob([`(${code})()`]);
    return new Worker(URL.createObjectURL(blob));
  }
}



相关文章
|
前端开发 API 开发者
harmonyOS基础- 快速弄懂HarmonyOS ArkTs基础组件、布局容器(前端视角篇)
本文由黑臂麒麟(6年前端经验)撰写,介绍ArkTS开发中的常用基础组件与布局组件。基础组件包括Text、Image、Button等,支持样式设置如字体颜色、大小和加粗等,并可通过Resource资源引用统一管理样式。布局组件涵盖Column、Row、List、Grid和Tabs等,支持灵活的主轴与交叉轴对齐方式、分割线设置及滚动事件监听。同时,Tabs组件可实现自定义样式与页签切换功能。内容结合代码示例,适合初学者快速上手ArkTS开发。参考华为开发者联盟官网基础课程。
1326 75
harmonyOS基础- 快速弄懂HarmonyOS ArkTs基础组件、布局容器(前端视角篇)
|
前端开发 安全 开发工具
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
1054 90
【11】flutter进行了聊天页面的开发-增加了即时通讯聊天的整体页面和组件-切换-朋友-陌生人-vip开通详细页面-即时通讯sdk准备-直播sdk准备-即时通讯有无UI集成的区别介绍-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
551 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
数据采集 前端开发 JavaScript
《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件
本文档详细介绍了从零开始搭建一个包含前后端交互的热搜展示项目的全过程。通过本教程,读者不仅能学习到完整的项目开发流程,还能掌握爬虫技术和前后端交互的具体实践。适合有一定编程基础并对项目实战感兴趣的开发者参考。
355 1
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
669 0
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
547 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
监控 前端开发 数据可视化
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
@icraft/player-react 是 iCraft Editor 推出的 React 组件库,旨在简化3D数字孪生场景的前端集成。它支持零配置快速接入、自定义插件、丰富的事件和方法、动画控制及实时数据接入,帮助开发者轻松实现3D场景与React项目的无缝融合。
1105 9
3D架构图软件 iCraft Editor 正式发布 @icraft/player-react 前端组件, 轻松嵌入3D架构图到您的项目,实现数字孪生
|
SpringCloudAlibaba JavaScript 前端开发
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
分布式组件、nacos注册配置中心、openfegin远程调用、网关gateway、ES6脚本语言规范、vue、elementUI
谷粒商城笔记+踩坑(2)——分布式组件、前端基础,nacos+feign+gateway+ES6+vue脚手架
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
332 1
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
488 2

热门文章

最新文章

  • 1
    前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
    1175
  • 2
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(九):强势分析Animation动画各类参数;从播放时间、播放方式、播放次数、播放方向、播放状态等多个方面,完全了解CSS3 Animation
    519
  • 3
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(八):学习transition过渡属性;本文学习property模拟、duration过渡时间指定、delay时间延迟 等多个参数
    403
  • 4
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(七):学习ransform属性;本文学习 rotate旋转、scale缩放、skew扭曲、tanslate移动、matrix矩阵 多个参数
    397
  • 5
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(六):全方面分析css的Flex布局,从纵、横两个坐标开始进行居中、两端等元素分布模式;刨析元素间隔、排序模式等
    513
  • 6
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(五):背景属性;float浮动和position定位;详细分析相对、绝对、固定三种定位方式;使用浮动并清除浮动副作用
    685
  • 7
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(四):元素盒子模型;详细分析边框属性、盒子外边距
    1224
  • 8
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(三):元素继承关系、层叠样式规则、字体属性、文本属性;针对字体和文本作样式修改
    276
  • 9
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(二):CSS伪类:UI伪类、结构化伪类;通过伪类获得子元素的第n个元素;创建一个伪元素展示在页面中;获得最后一个元素;处理聚焦元素的样式
    1021
  • 10
    【CSS】前端三大件之一,如何学好?从基本用法开始吧!(一):CSS发展史;CSS样式表的引入;CSS选择器使用,附带案例介绍
    476