倒计时功能分享

news/2024/11/20 5:41:30/

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

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

  • setInterval

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

    javascript">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 后,我们来看下代码:

    javascript">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 来优化一下之前的代码:

    javascript">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差值计算

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

    javascript">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点几秒,在实际体验中用户完全感受不出来。我想说的是,细节决定成败,有可能这零点几秒的内容就决定了面试的成败。如果做什么事都只做个差不多,那你永远不会有自己的"核心科技"。


http://www.ppmy.cn/news/1548420.html

相关文章

蓝桥杯每日真题 - 第14天

题目&#xff1a;&#xff08;2022&#xff09; 题目描述&#xff08;13届 C&C B组A题&#xff09; 解题思路&#xff1a; 定义状态&#xff1a; 使用一个二维数组 dp[j][k] 来表示将数字 k 拆分为 j 个不同正整数的方案数。 初始化&#xff1a; 初始状态设定为 dp[0][0]…

实用教程:如何无损修改MP4视频时长

如何在UltraEdit中搜索MP4文件中的“mvhd”关键字 引言 在视频编辑和分析领域&#xff0c;有时我们需要深入到视频文件的底层结构中去。UltraEdit&#xff08;UE&#xff09;和UEStudio作为强大的文本编辑器&#xff0c;允许我们以十六进制模式打开和搜索MP4文件。本文将指导…

鸿蒙生态开发以及技术栈介绍

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;鸿蒙开发篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来鸿蒙开发篇专栏内容: 有没有可以2小时不用手机的&#xff1f; 打开电视用什么&#xff1f; 打开空调用什么&a…

React教程第二节之虚拟DOM与Diffing算法理解

1、什么是虚拟DOM 虚拟DOM 是javascript的一个对象&#xff0c;是内存中的一种数据结构&#xff0c;以树的形式存储UI的状态&#xff0c;树中的每个节点都代表着真实的DOM&#xff0c;用来描述我们希望在页面看到的 HTML结构&#xff1b; 现在的MVVM 框架&#xff0c;大多使用…

CSM32RV20:RISC-V核的低功耗MCU芯片,常用在智能门锁上

CSM32RV20是一款基于RISC-V核的低功耗MCU芯片。 内置RISC-V RV32IMAC内核&#xff08;2.6CoreMark/MHz&#xff09;&#xff1b; 蕞高32MHz工作频率&#xff1b; 内置4kB的SRAM&#xff1b; 内置8B的ALWAYS寄存器&#xff0c;能在掉电模式2下保存数据&#xff1b; 内置40kB的嵌…

区块链中的wasm合约是什么?

概述 让我们梳理一下WASM合约的概念和重要性…这涉及到区块链和智能合约的发展。 WASM(WebAssembly)本身是一种低级的类汇编语言&#xff0c;最初是为Web浏览器设计的。将它引入区块链领域是一个重要创新。 相比传统的智能合约(如Solidity)&#xff0c;WASM有很多优势&#…

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

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

泷羽sec-安全见闻(9)

安全见闻&#xff08;9&#xff09; 声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其…