倒计时功能分享
今天想要分享的是一个面试题,也是一个我们在项目中常用的功能:倒计时。
首先我们在写倒计时的时候必须要考虑到是:准确性、性能。接下来我们一步一步实现这个完美地倒计时功能。
-
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 执行的总时间 |
|---|---|---|---|
| 0 | 0 | 1000 | 0 |
| 1 | 1200 | 800 | 1200 |
| 2 | 1100 | 700 | 2300 |
| 3 | 1000 | 700 | 3300 |
| 4 | 2200 | 500 | 5500 |
| 5 | 1300 | 200 | 6800 |
| 6 | 1200 | 1000 | 8000 |
| … | … | … | … |
从中可以看到:下次执行的时间 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点几秒,在实际体验中用户完全感受不出来。我想说的是,细节决定成败,有可能这零点几秒的内容就决定了面试的成败。如果做什么事都只做个差不多,那你永远不会有自己的"核心科技"。
相关文章:
倒计时功能分享
今天想要分享的是一个面试题,也是一个我们在项目中常用的功能:倒计时。 首先我们在写倒计时的时候必须要考虑到是:准确性、性能。接下来我们一步一步实现这个完美地倒计时功能。 setInterval 先来简单实现一个倒计时的函数: func…...
【论文分享】使用多源数据识别建筑功能:以中国三大城市群为例
建筑功能对城市规划至关重要,而利用多源数据进行建筑功能分类有助于支持城市规划政策。本研究通过分析建筑特征和POI密度,识别了中国三个城市群的建筑功能,并使用XGBoost模型验证了其在大规模映射中的高准确性和有效性。研究强调了建筑环境对…...
华为手机启用ADB无线调试功能
打开开发者模式,勾选USB调试,和“仅充电”模式下允许ADB调试 确认 设置添加adb路径到PATH变量 使用adb查看安卓设置 切换为无线模式: 查看手机IP...
云原生之Kubernetes集群搭建
1、Kubernetets基础概念 传统的服务器架构演进,现在基于docker容器化应用可以完成快速部署,但是对于大型的应用,有可能出现成百上千个容器化应用,一个挂了需要人工管理是相当麻烦,因此急需一个大规模容器编排系统。 Kubernetes Kubernetes 是一个可移植、可扩展的开源平…...
STM32单片机CAN总线汽车线路通断检测
目录 目录 前言 一、本设计主要实现哪些很“开门”功能? 二、电路设计原理图 1.电路图采用Altium Designer进行设计: 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 随着汽车电子技术的不断发展,车辆通信接口在汽车电子控…...
大连理工大学概率上机作业免费下载
大连理工大学概率论与数理统计上机资源 本资源库收录了大连理工大学概率论与数理统计课程的上机作业范例代码,旨在通过实际操作加深学生对概率统计概念的理解,帮助学生更好地理解和掌握知识点。 作业内容概览 第一题:随机变量关系探索 数…...
Tomcat 如何管理 Session
Tomcat 如何管理 Session 我们知道,Tomcat 中每一个 Context 容器对应一个 Web 应用,而 Web 应用之间的 Session 应该是独立的,因此 Session 的管理肯定是 Context 级的,也就是一个 Context 一定关联多个 Session。 Tomcat 中主…...
stm32启动过程解析startup启动文件
1.STM32的启动过程模式 1.1 根据boot引脚决定三种启动模式 复位后,在 SYSCLK 的第四个上升沿锁存 BOOT 引脚的值。BOOT0 为专用引脚,而 BOOT1 则与 GPIO 引脚共用。一旦完成对 BOOT1 的采样,相应 GPIO 引脚即进入空闲状态,可用于…...
SystemVerilog学习——构造函数new
一、概述 在 SystemVerilog 中,new 是一个构造函数,用于创建类的实例(即对象)。它在面向对象编程(OOP)中起着重要作用,负责实例化一个对象并进行初始化。与传统编程语言(如 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 集合框架(Collections Framework)1.2 输入输出流(I/O Streams)1.3 并发编程(Concurrency)1.4 反射(Reflection)1.5 泛型&…...
esp32c3开发板通过micropython的ubluetooth库连蓝牙设备
ESP32-C3开发板是一款高性能、低功耗的微控制器,搭载了Espressif自家的RISC-V处理器。通过MicroPython,一种面向微控制器的精简版Python编程语言,开发者可以轻松地为ESP32-C3编写代码。MicroPython的ubluetooth库使得ESP32-C3能够通过蓝牙与各…...
leetcode hot100【LeetCode 35.搜索插入位置】java实现
LeetCode 35.搜索插入位置 题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用 O(log n) 的时间复杂度来实现。 示例 1: 输入: nums [1,3,5,6…...
我们要用平凡来诠释非凡
#孟晚舟香港中文大学演讲# #华为价值观念# #并非站在山顶才能被看见# #传递正确的价值观# #如果信仰有颜色,那一定是中国红# #送给自己的价值理念# 在信息大爆炸的时代,很多同学都希望尽可能的抓取更多的知识,尽可能的不要遗漏任何热点…...
synchronized和volatile区别
synchronized和volatile是Java并发编程中两种重要的同步机制,它们之间存在明显的区别。以下是对这两者的详细比较: 一、基本定义与作用 synchronized 是一个用于实现线程同步的关键字。可以用来锁住方法或代码块,从而确保在同一时刻只有一个…...
125.验证回文串-力扣(LeetCode)
题目: 解题思路: 首先进行移除非字母数字字符,并将大写字符转换为小写字符的操作。这个过程中,主要利用快慢指针的方式来进行移除操作,通过加32将大写字符转换为小写字符。完成后,将前一半的数据与后一半的…...
线程间通信:wait和notify
线程间通信:wait和notify 1、Object的wait和notify方法 Java中的Object类提供了两个重要的方法,用于线程间的通信和同步:wait()方法和notify()方法 wait()方法的定义 方法签名:public final void wait() throws InterruptedEx…...
风险识别和管理的工具
1.风险识别工具和根本原因识别在项目管理中非常重要,常用的工具包括 因果图根本原因识别RCA鱼骨图 因果图 因果图是一种图形工具,用于识别问题或风险的根本原因。它通过将问题或风险因素与可能的根本原因联系起来,帮助团队更深入地了解问…...
qt之QFTP对文件夹(含嵌套文件夹和文件)、文件删除下载功能
一、前言 主要功能如下: 1.实现文件夹的下载和删除,网上很多资料都是单独对某个路径的文件操作的,并不能对文件夹操作 2.实现目标机中含中文名称自动转码,有些系统编码方式不同,下载出来的文件会乱码 3.实现ftp功能…...
为何数据库推荐将IPv4地址存储为32位整数而非字符串?
目录 一、IPv4地址在数据库中的存储方式? 二、IPv4地址的存储方式比较 (一)字符串存储 vs 整数存储 (二)IPv4地址"192.168.1.8"说明 三、数据库推荐32位整数存储方式原理 四、存储方式对系统性能的影响…...
中小团队如何利用Taotoken统一管理多个项目的AI调用成本
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 中小团队如何利用Taotoken统一管理多个项目的AI调用成本 对于同时推进多个AI应用开发项目的中小型技术团队而言,管理分…...
MCP Analytics Suite:用自然语言驱动AI数据分析,零代码生成专业报告
1. 项目概述:当AI助手遇上专业数据分析如果你和我一样,日常工作中需要处理大量的业务数据——可能是Shopify的订单报表、Stripe的支付流水,或者是一堆从各个渠道导出的CSV文件——那你一定体会过那种“数据在手,却无从下手”的焦虑…...
[具身智能-694]:万物皆智能,万物皆 ROS2:未来所有带感知、能运动、可交互的硬件终端,都能用 ROS2 做底座,智能普惠全域设备。万物接入 ROS2,就是接入标准化、开源化、互联化的智能时代。
一、为什么说「万物皆智能」从传统机电设备 → 感知 决策 执行一体化:普通家电、工业设备、移动载体、穿戴设备、楼宇设施,都在加传感器、算力、通信、自主决策,不再是被动受控,而是具备自主感知、逻辑判断、联动协作的智能属性…...
观察Taotoken用量看板如何帮助团队透明化管理API成本
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 观察Taotoken用量看板如何帮助团队透明化管理API成本 作为团队的技术负责人,管理大模型API成本是一项持续且细致的工作…...
如何在Windows电脑上直接安装Android应用:3个简单步骤告别模拟器
如何在Windows电脑上直接安装Android应用:3个简单步骤告别模拟器 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 你是否曾经希望在Windows电脑上直接运行An…...
抖音无水印下载器:终极免费批量下载工具完全指南
抖音无水印下载器:终极免费批量下载工具完全指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖…...
GitHub企业版MCP服务器:为AI助手集成私有化GitHub工作流
1. 项目概述:一个为开发者定制的GitHub企业版MCP服务器如果你是一名重度依赖GitHub Enterprise进行团队协作的开发者,并且正在探索如何将AI助手(比如Claude、Cursor等)无缝集成到你的日常开发工作流中,那么你很可能已经…...
构建AI智能体技能超市:标准化工作流与多平台适配实践
1. 项目概述:一个面向AI智能体的“技能超市”如果你和我一样,每天都在和Codex、Claude、Cursor这些AI助手打交道,那你肯定也遇到过这样的场景:想让AI帮你生成一份规范的Git提交信息、自动更新文档索引,或者为一个新项目…...
ARM HCR_EL2寄存器解析与虚拟化控制
1. ARM HCR_EL2寄存器架构解析HCR_EL2(Hypervisor Configuration Register)是ARMv8/v9架构中用于控制虚拟化行为的关键系统寄存器。作为Hypervisor的主要控制接口,它定义了EL2对低特权级(EL1/EL0)执行环境的监控策略。…...
在Android Termux中搭建轻量级Docker容器环境:原理、部署与实战
1. 项目概述与核心价值最近在折腾移动设备上的开发环境,发现一个挺有意思的项目:George-Seven/Termux-Udocker。简单来说,它是在Android平台的Termux终端模拟器里,实现一个轻量级的Docker容器运行环境。这玩意儿解决了一个挺实际的…...
