当前位置: 首页 > news >正文

倒计时功能分享

今天想要分享的是一个面试题,也是一个我们在项目中常用的功能:倒计时。

首先我们在写倒计时的时候必须要考虑到是:准确性性能。接下来我们一步一步实现这个完美地倒计时功能。

  • setInterval

    先来简单实现一个倒计时的函数:

    function example1(leftTime) {let t = leftTime;setInterval(() => {t = t - 1000;console.log(t);}, 1000);
    }example1(10);
    

    可以看到使用 setInterval 即可,但是setInterval 真的准确吗?我们来看一下 MDN 中的说明:

    如果你的代码逻辑执行时间可能比定时器时间间隔要长,建议你使用递归调用了 setTimeout() 的具名函数

    例如,使用 setInterval() 以 5 秒的间隔轮询服务器,可能因网络延迟、服务器无响应以及许多其他的问题而导致请求无法在分配的时间内完成。

    简单来说意思就是,js 因为是单线程的原因,如果前面有阻塞线程的任务,那么就可能会导致 setInterval 函数延迟,这样倒计时就肯定会不准确,建议使用 setTimeout 替换 setInterval

  • setTimeout

    按照上述的建议将 setInterval 换为 setTimeout 后,我们来看下代码:

    function example2(leftTime) {let t = leftTime;setTimeout(() => {t = t - 1000;if (t > 0) {console.log(t);example2(t);}console.log(t);}, 1000);
    }
    

    MDN 中也说了,有很多因素会导致 setTimeout 的回调函数执行比设定的预期值更久,比如嵌套超时、非活动标签超时、追踪型脚本的节流、超时延迟等等。 总之呢就是和 setInterval 差不多,时间一长,就会有误差出现,而且setTimeout有一个很不好的点在于,当你的程序在后台运行时,setTimeout也会一直执行,这样会严重的而浪费性能,那么有什么办法可以解决这种问题吗?

  • requestAnimationFrame

    这里就不得不提一个新的方法 requestAnimationFrame,它是一个浏览器 API,允许以 60 帧/秒 (FPS) 的速率请求回调,而不会阻塞主线程。通过调用 requestAnimationFrame 方法浏览器会在下一次重绘之前执行指定的函数,这样可以确保回调在每一帧之间都能够得到适时的更新。 这个 API 我们在大屏可视化项目中需要动态切换图表数据时也是常用的。

    那么我们使用 requestAnimationFrame 结合 setTimeout 来优化一下之前的代码:

    function example4(leftTime) {let t = leftTime;function start() {requestAnimationFrame(() => {t = t - 1000;setTimeout(() => {console.log(t);start();}, 1000);});}start();
    }
    
    为什么要使用 requestAnimationFrame + setTimeout呢?

    原因一个是息屏或者切后台的操作时,requestAnimationFrame 是不会继续调用函数的,但是如果只使用requestAnimationFrame 的话,函数相当于 1 秒的时候要调用 60 次,太浪费性能。

    在切后台或者息屏的实际操作执行时会发现,当回到页面时,倒计时会接着切后台时的时间执行,而没有更新到最新的时间,这样的bug是接受不了的。

  • diffTime差值计算

    要解决上述的问题,最通用的办法就是通过时间差值每次进行对比就可以了。

    function example5(leftTime) {const now = performance.now();function start() {setTimeout(() => {const diff = leftTime - (performance.now() - now);console.log(diff);requestAnimationFrame(start);}, 1000);}start();
    }
    

    上面的代码实现思路其实在实际的业务中已经能够满足我们的使用场景,但其实还是没有解决setTimeout会延迟的问题,当线程被占用之后,很容易出现误差,那么有什么更新的办法进行处理呢?

最佳方案

先要明确的是,setTimeout函数中执行代码的时间肯定是要大于等于setTimeout时间的,那么就可能出现设定的 1 秒,实际执行却执行了 2 秒的情况,那么我们的实现思路也很简单,每次计算一下setTimeout实际执行的时间,然后动态的调整下一次执行的时间,而不是设置固定的值。

我们来用图表举例推演一下每次执行的情况:

第n次执行executionTime 实际执行时间nextTime 下次需要执行的时间totleTime 执行的总时间
0010000
112008001200
211007002300
310007003300
422005005500
513002006800
6120010008000

从中可以看到:下次执行的时间 nextTime = 1000 - totleTime % 1000;这样我们就可以得出下次执行的时间,从而每次都去动态的调整多余消耗的时间,大大减小倒计时最终的误差

还有需要考虑的是,实际业务中返回的剩余时间肯定不会是整数,所以我们的第一次执行的时间最好可以先让剩余时间变为整数,这样可以在倒计时到最后一秒时更加的精确。

根据上述的思路来看一下最终封装出来的 react hooks:

const useCountDown = ({ leftTime, ms = 1000, onEnd }) => {const countdownTimer = useRef();const startTimer = useRef();//记录初始时间const startTimeRef = useRef(performance.now());// 第一次执行的时间处理,让下一次倒计时时调整为整数const nextTimeRef = useRef(leftTime % ms);const [count, setCount] = useState(leftTime);const clearTimer = () => {countdownTimer.current && clearTimeout(countdownTimer.current);startTimer.current && clearTimeout(startTimer.current);};const startCountDown = () => {clearTimer();const currentTime = performance.now();// 算出每次实际执行的时间const executionTime = currentTime - startTimeRef.current;// 实际执行时间大于上一次需要执行的时间,说明执行时间多了,否则需要补上差的时间const diffTime =executionTime > nextTimeRef.current? executionTime - nextTimeRef.current: nextTimeRef.current - executionTime;setCount((count) => {const nextCount =count - (Math.floor(executionTime / ms) || 1) * ms - nt;return nextCount <= 0 ? 0 : nextCount;});// 算出下一次的时间nextTimeRef.current =executionTime > nextTimeRef.current ? ms - diffTime : ms + diffTime;// 重置初始时间startTimeRef.current = performance.now();countdownTimer.current = setTimeout(() => {requestAnimationFrame(startCountDown);}, nextTimeRef.current);};useEffect(() => {setCount(leftTime);startTimer.current = setTimeout(startCountDown, nextTimeRef.current);return () => {clearTimer();};}, [leftTime]);useEffect(() => {if (count <= 0) {clearTimer();onEnd && onEnd();}}, [count]);return count;
};export default useCountDown;

如果想要封装组件的话,可以在hooks的基础上进行二次封装。

到这里,肯定会有人说,做了这么多的操作,有必要吗,就算差0点几秒,在实际体验中用户完全感受不出来。我想说的是,细节决定成败,有可能这零点几秒的内容就决定了面试的成败。如果做什么事都只做个差不多,那你永远不会有自己的"核心科技"。

相关文章:

倒计时功能分享

今天想要分享的是一个面试题&#xff0c;也是一个我们在项目中常用的功能&#xff1a;倒计时。 首先我们在写倒计时的时候必须要考虑到是&#xff1a;准确性、性能。接下来我们一步一步实现这个完美地倒计时功能。 setInterval 先来简单实现一个倒计时的函数&#xff1a; func…...

【论文分享】使用多源数据识别建筑功能:以中国三大城市群为例

建筑功能对城市规划至关重要&#xff0c;而利用多源数据进行建筑功能分类有助于支持城市规划政策。本研究通过分析建筑特征和POI密度&#xff0c;识别了中国三个城市群的建筑功能&#xff0c;并使用XGBoost模型验证了其在大规模映射中的高准确性和有效性。研究强调了建筑环境对…...

华为手机启用ADB无线调试功能

打开开发者模式,勾选USB调试,和“仅充电”模式下允许ADB调试 确认 设置添加adb路径到PATH变量 使用adb查看安卓设置 切换为无线模式: 查看手机IP...

云原生之Kubernetes集群搭建

1、Kubernetets基础概念 传统的服务器架构演进,现在基于docker容器化应用可以完成快速部署,但是对于大型的应用,有可能出现成百上千个容器化应用,一个挂了需要人工管理是相当麻烦,因此急需一个大规模容器编排系统。 Kubernetes Kubernetes 是一个可移植、可扩展的开源平…...

STM32单片机CAN总线汽车线路通断检测

目录 目录 前言 一、本设计主要实现哪些很“开门”功能&#xff1f; 二、电路设计原理图 1.电路图采用Altium Designer进行设计&#xff1a; 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 随着汽车电子技术的不断发展&#xff0c;车辆通信接口在汽车电子控…...

大连理工大学概率上机作业免费下载

大连理工大学概率论与数理统计上机资源 本资源库收录了大连理工大学概率论与数理统计课程的上机作业范例代码&#xff0c;旨在通过实际操作加深学生对概率统计概念的理解&#xff0c;帮助学生更好地理解和掌握知识点。 作业内容概览 第一题&#xff1a;随机变量关系探索 数…...

Tomcat 如何管理 Session

Tomcat 如何管理 Session 我们知道&#xff0c;Tomcat 中每一个 Context 容器对应一个 Web 应用&#xff0c;而 Web 应用之间的 Session 应该是独立的&#xff0c;因此 Session 的管理肯定是 Context 级的&#xff0c;也就是一个 Context 一定关联多个 Session。 Tomcat 中主…...

stm32启动过程解析startup启动文件

1.STM32的启动过程模式 1.1 根据boot引脚决定三种启动模式 复位后&#xff0c;在 SYSCLK 的第四个上升沿锁存 BOOT 引脚的值。BOOT0 为专用引脚&#xff0c;而 BOOT1 则与 GPIO 引脚共用。一旦完成对 BOOT1 的采样&#xff0c;相应 GPIO 引脚即进入空闲状态&#xff0c;可用于…...

SystemVerilog学习——构造函数new

一、概述 在 SystemVerilog 中&#xff0c;new 是一个构造函数&#xff0c;用于创建类的实例&#xff08;即对象&#xff09;。它在面向对象编程&#xff08;OOP&#xff09;中起着重要作用&#xff0c;负责实例化一个对象并进行初始化。与传统编程语言&#xff08;如 C 或 Jav…...

力扣题目总结

1.游戏玩法分析IV AC: select IFNULL(round(count(distinct(Result.player_id)) / count(distinct(Activity.player_id)), 2), 0) as fraction from (select Activity.player_id as player_idfrom (select player_id, DATE_ADD(MIN(event_date), INTERVAL 1 DAY) as second_da…...

Java API 进阶指南:从核心API到高级应用的全面提升

文章目录 Java API 进阶学习指南1. 深入理解核心API1.1 集合框架&#xff08;Collections Framework&#xff09;1.2 输入输出流&#xff08;I/O Streams&#xff09;1.3 并发编程&#xff08;Concurrency&#xff09;1.4 反射&#xff08;Reflection&#xff09;1.5 泛型&…...

esp32c3开发板通过micropython的ubluetooth库连蓝牙设备

ESP32-C3开发板是一款高性能、低功耗的微控制器&#xff0c;搭载了Espressif自家的RISC-V处理器。通过MicroPython&#xff0c;一种面向微控制器的精简版Python编程语言&#xff0c;开发者可以轻松地为ESP32-C3编写代码。MicroPython的ubluetooth库使得ESP32-C3能够通过蓝牙与各…...

leetcode hot100【LeetCode 35.搜索插入位置】java实现

LeetCode 35.搜索插入位置 题目描述 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用 O(log n) 的时间复杂度来实现。 示例 1: 输入: nums [1,3,5,6…...

我们要用平凡来诠释非凡

#孟晚舟香港中文大学演讲# #华为价值观念# #并非站在山顶才能被看见# #传递正确的价值观# #如果信仰有颜色&#xff0c;那一定是中国红# #送给自己的价值理念# 在信息大爆炸的时代&#xff0c;很多同学都希望尽可能的抓取更多的知识&#xff0c;尽可能的不要遗漏任何热点…...

synchronized和volatile区别

synchronized和volatile是Java并发编程中两种重要的同步机制&#xff0c;它们之间存在明显的区别。以下是对这两者的详细比较&#xff1a; 一、基本定义与作用 synchronized 是一个用于实现线程同步的关键字。可以用来锁住方法或代码块&#xff0c;从而确保在同一时刻只有一个…...

125.验证回文串-力扣(LeetCode)

题目&#xff1a; 解题思路&#xff1a; 首先进行移除非字母数字字符&#xff0c;并将大写字符转换为小写字符的操作。这个过程中&#xff0c;主要利用快慢指针的方式来进行移除操作&#xff0c;通过加32将大写字符转换为小写字符。完成后&#xff0c;将前一半的数据与后一半的…...

线程间通信:wait和notify

线程间通信&#xff1a;wait和notify 1、Object的wait和notify方法 Java中的Object类提供了两个重要的方法&#xff0c;用于线程间的通信和同步&#xff1a;wait()方法和notify()方法 wait()方法的定义 方法签名&#xff1a;public final void wait() throws InterruptedEx…...

风险识别和管理的工具

1.‌风险识别工具和根本原因识别在项目管理中非常重要&#xff0c;常用的工具包括 因果图根本原因识别RCA鱼骨图 因果图 因果图是一种图形工具&#xff0c;用于识别问题或风险的根本原因。它通过将问题或风险因素与可能的根本原因联系起来&#xff0c;帮助团队更深入地了解问…...

qt之QFTP对文件夹(含嵌套文件夹和文件)、文件删除下载功能

一、前言 主要功能如下&#xff1a; 1.实现文件夹的下载和删除&#xff0c;网上很多资料都是单独对某个路径的文件操作的&#xff0c;并不能对文件夹操作 2.实现目标机中含中文名称自动转码&#xff0c;有些系统编码方式不同&#xff0c;下载出来的文件会乱码 3.实现ftp功能…...

为何数据库推荐将IPv4地址存储为32位整数而非字符串?

目录 一、IPv4地址在数据库中的存储方式&#xff1f; 二、IPv4地址的存储方式比较 &#xff08;一&#xff09;字符串存储 vs 整数存储 &#xff08;二&#xff09;IPv4地址"192.168.1.8"说明 三、数据库推荐32位整数存储方式原理 四、存储方式对系统性能的影响…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现

目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...

Flask RESTful 示例

目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题&#xff1a; 下面创建一个简单的Flask RESTful API示例。首先&#xff0c;我们需要创建环境&#xff0c;安装必要的依赖&#xff0c;然后…...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩

目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

基于服务器使用 apt 安装、配置 Nginx

&#x1f9fe; 一、查看可安装的 Nginx 版本 首先&#xff0c;你可以运行以下命令查看可用版本&#xff1a; apt-cache madison nginx-core输出示例&#xff1a; nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

渲染学进阶内容——模型

最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

cf2117E

原题链接&#xff1a;https://codeforces.com/contest/2117/problem/E 题目背景&#xff1a; 给定两个数组a,b&#xff0c;可以执行多次以下操作&#xff1a;选择 i (1 < i < n - 1)&#xff0c;并设置 或&#xff0c;也可以在执行上述操作前执行一次删除任意 和 。求…...

python如何将word的doc另存为docx

将 DOCX 文件另存为 DOCX 格式&#xff08;Python 实现&#xff09; 在 Python 中&#xff0c;你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是&#xff0c;.doc 是旧的 Word 格式&#xff0c;而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

Python如何给视频添加音频和字幕

在Python中&#xff0c;给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加&#xff0c;包括必要的代码示例和详细解释。 环境准备 在开始之前&#xff0c;需要安装以下Python库&#xff1a;…...