前言
欢送关注同名公众号《
熊的猫
》,文章会同步更新,也可疾速退出前端交换群!
最近有个同学在面试时被要求手写一个 CountDown 计时器组件 ,但可能是因为之前没有理解过,所以思路上没有那么顺畅,过后他询问我应该怎么写(),于是就有了本篇文章, 心愿本篇文章对你有所帮忙!!! 哈哈,我也没写过
不难看出,要求手写一个 CountDown 计时器组件 目标无非考查如下几个方面(): 谁也不晓得面试官在想什么
-
组件封装能力
- 组件输出 ,即对应组件 外部 Props 的 设计 和 考量
- 组件输入 ,即对应组件 对外提供 的 属性 或 办法
- 逻辑复用 ,即指组件外部逻辑的 可组合性
-
工夫相干的敏感度
- 倒计时的实现形式有多种,例如
setInterval、setTimeout、requestAnimationFrame
等等,那么哪种更适合? - 获取以后工夫能够用
Date.now()、performance.now()
,那么该怎么选?
- 倒计时的实现形式有多种,例如
setInterval & setTimeout & requestAnimationFrame
倒计时性能必然须要一个一直执行的 异步过程 (),这能够应用运行时环境提供的 API,即 setInterval、setTimeout、requestAnimationFrame,那么到底该抉择谁更适合呢? 没疑难吧
上面进行一一剖析!
setInterval
是什么?
setInterval()
办法会反复调用 一个函数 或 执行一个代码片段,在每次调用之间具备固定的工夫距离,并会返回一个 interval ID
用于标识惟一的工夫距离,可通过调用 clearInterval()
“) 来移除定时器。
共享同一个 ID 池
值得注意的是,setInterval()
和 setTimeout()
是 共享同一个 ID 池 的,所以说 clearInterval()
和 clearTimeout()
“) 在技术上是可 调换应用 的:
<template>
<div class="count-down">
<h1>Count through setInterval:{{countInterval}}</h1>
<button @click="stopInterval">Stopping through clearTimeout</button>
<hr>
<h1>Count through setTimeout:{{countTimeout}}</h1>
<button @click="stopTimeout">Stopping through clearInterval</button>
</div>
</template>
<script setup lang='ts'>
import {ref} from 'vue'
// 1. Example for setInterval
const countInterval = ref(0)
const IntervalID = setInterval(() => countInterval.value++, 1000)
const stopInterval = () => {console.log('ClearTimeout triggered in stopInterval method!')
clearTimeout(IntervalID)
}
// 2. Example for setTimeout
const countTimeout = ref(0)
let TimeoutID = 0
const addCount = () => {TimeoutID = setTimeout(() => {
countTimeout.value++
addCount()}, 1000)
}
addCount()
const stopTimeout = () => {console.log('ClearInterval triggered in stopTimeout method!')
clearInterval(TimeoutID)
}
</script>
但为了 防止代码横七竖八 、 保障代码的可维护性,还是更举荐应用互相匹配的 clearInterval()
和 clearTimeout()
。
提早限度
当 setInterval()
定时器是产生 嵌套应用 时,且 嵌套超过 5
层深度 时:
- 浏览器将 主动强制 设置定时器的
最小工夫距离为 4 毫秒
- 若尝试将 深层嵌套 中调用
setInterval()
的提早设定为小于 4 毫秒
的值,其将 被固定为4
毫秒
浏览器这样的行为会使得 setInterval()
产生提早性,起因是 为了加重嵌套定时器对性能产生的潜在影响。
setTimeout + 递归 更适合?
如果 代码逻辑执行工夫 可能大于 定时器工夫距离 ,那么倡议你应用 递归调用setTimeout()
的形式来实现。
(function loop(){setTimeout(function() {
// Your logic here
loop();}, delay);
})();
例如,如果你要应用 setInterval()
以 5s
轮询服务器,可能因 网络提早、服务器无响应 或许多其余的问题而导致申请 无奈在指定工夫内实现 ,因而可能会呈现排队的 XHR 申请 没有按程序返回 的问题。
setTimeout
是什么?
setTimeout()
办法用于设置一个定时器,该定时器在 定时器到期后 执行 一个函数 或 指定的一段代码 ,并且会返回一个 正整数 的 timeoutID
,示意由 setTimeout()
调用创立的定时器的编号,可通过调用 clearTimeout()
来勾销定时器。
最大延时值
浏览器外部以 32 位带符号整数 存储延时,这会导致如果一个延时大于 2147483647 ms(大概 24.8 天)
时会产生溢出,导致定时器将会被 立刻执行,这个限度实用于 setInterval()
和 setTimeout()
。
延时比指定值更长的起因
有很多因素会导致 setTimeout
的 回调函数 执行 比设定的预期值更久:
-
嵌套超时
- 一旦对
setTimeout
的 嵌套调用达到5
层,浏览器将强制执行4 毫秒的最小超时
- 一旦对
-
非流动标签的超时
- 为了优化 后盾标签的加载损耗(如 升高耗电量),浏览器会在非流动标签中强制执行一个 最小的超时提早
- 例如,
Firefox
桌面版 和Chrome
不流动标签都有一个1s
的最小超时值 - 例如,安卓版
Firefox
浏览器对不流动的标签有一个至多15m
的超时,并可能齐全卸载它们 - 例如,若标签中蕴含
AudioContext
,Firefox
不会对非流动标签进行节流
-
追踪型脚本的节流
- 例如,
Firefox
对它辨认为追踪型脚本的脚本 施行额定节流 ,即当在 前台运行 时, 节流的最小提早是4ms
- 当在 后盾标签 中, 节流的最小提早是
10000ms(即 10s)
,在文档首次加载后30s
开始失效
- 例如,
-
在加载页面时推延超时
- 以后标签页正在加载时,
Firefox
将推延触发setTimeout()
计时器,直到主线程被认为是闲暇 的(相似于window.requestIdleCallback()
)或 直到 加载事件触发结束,才开始触发
- 以后标签页正在加载时,
requestAnimationFrame
是什么?
window.requestAnimationFrame()
会通知浏览器咱们心愿执行一个 动画 ,并且要求浏览器在下次 重绘之前 调用指定的回调函数 更新动画,即每 16.67ms
执行一次回调函数。
回调函数的参数
回调办法在会接管到一个 DOMHighResTimeStamp
参数,它是一个 十进制数,单位为毫秒,最小精度为 1ms(1000μs)
。
同一帧 中的 多个回调函数 都会承受到一个 雷同的工夫戳 ,即便在计算上一个回调函数的工作负载期间曾经耗费了一些工夫,因而要确保总是应用 第一个参数(或其余一些获取以后工夫的办法) 来计算动画在一帧中的进度,否则动画在 高刷新率 的屏幕中会 运行得更快。
暂停调用
为了进步性能和电池寿命,在大多数浏览器里,当 requestAnimationFrame()
运行在 后盾标签页 或 暗藏的 <iframe>
里时,requestAnimationFrame()
会被暂停调用以晋升性能和电池寿命。
到底选谁?
从以上内容来看,仿佛没有一个完满的计划呀,这不是更加大难度了?
莫慌!既然都不完满,那么也要从矮个子中挑个高个子。
先不思考别的,setInterval 和 setTimeout 有一个致命的毛病:
-
最大延时限度
- 延时工夫一旦大于
2147483647 ms(约 24.8 天)
时会产生溢出,导致定时器将会被 立刻执行
- 延时工夫一旦大于
别的不说,就这个毛病就导致应用它们来做倒计时组件不太事实,难不成不容许用户的倒计时超过 25 天 吗?
所以这里抉择 requestAnimationFrame() + 递归 来实现!
Date.now() & performance.now()
同样的情理,获取以后日期的工夫戳也有 Date.now() 和 performance.now() 两种形式,又该选谁呢?
Date.now()
是什么?
Date.now()
办法返回自 1970 年 1 月 1 日 00:00:00 (UTC)
到以后工夫的毫秒数。
工夫精度被升高
为了提供针对定时攻打和指纹追踪的爱护,Date.now()
的精度可能会依据浏览器的高级设置我的项目而被取整。
例如,在 Firefox
中,默认启用 privacy.reduceTimerPrecision
设置项,在 Firefox 59
中,默认被取整至 20 微秒
;在 Firefox 60
中,则被取整至 2 毫秒
。
performance.now()
是什么?
performance.now()
办法返回一个 double 类型 的、用于存储 毫秒级 的工夫值。
获取以后日期工夫戳
performance.now()
次要是用来形容 离散工夫点 或 一段时间 (两个离散工夫点间的时间差),因而它的返回值并不是当 前日期的工夫戳,即 performance.now()
!= Date.now()
。
但能够通过换算的形式失去,即
Date.now() ≈ performance.timing.navigationStart + performance.now()
// 示例
const t1 = performance.timing.navigationStart + performance.now()
const t2 = Date.now();
console.log(t2, t1);
// t2 = 1686534658865 t1 = 1686534658865.2
工夫精度升高
为了提供对定时攻打和指纹的爱护,performance.now()
的精度可能会依据浏览器的设置而被舍弃,在 Firefox
中,privacy.reduceTimerPrecision
偏好是默认启用的,默认值为 1ms
。
// 升高工夫精度 (1ms) 在 Firefox 60
performance.now();
// 8781416
// 8781815
// 8782206
// ...
// 升高工夫精度 当 `privacy.resistFingerprinting` 启用
performance.now();
// 8865400
// 8866200
// 8866700
// ...
到底该选谁?
好家伙,说白了就还是没有一个完满的抉择呗!
在这里选 Date.now()
,毕竟 performance.now()
还得做转换,还有一个起因是 vant-count-down 组件也是用的 Date.now()
()。借鉴借鉴
实现 CountDown 计时器组件
组件输出 — Props
针对一个 CountDown 计时器组件 的 props 应该要蕴含如下几个内容:
time
,即须要倒计时的工夫format
,即输入的工夫格局,反对 DD:HH:mm:ss:SSS 格局finish
事件,即倒计时完结时会被执行的事件slot
默认插槽,即须要展现的组件内容视图,可接管到外部的倒计时格局输入
其中工夫咱们能够间接限度为 工夫戳,数值类型,当然如果你想反对更多格局,能够本人在写一个办法解决容许内部传入的各种格局,但理论在组件外部应用时必然是放弃是同一种类型,因而在这里咱们间接限定类型,让内部去进行转换。
组件输入
因为是一个根本的 CountDown 计时器组件,咱们能够不思考那么多输入,但至多要向内部裸露如下两个内容:
start()
办法,便于应用时能够基于任意工夫开始进行倒计时-
格式化的倒计时,便于内部间接用于展现解决,或自定义展现,返回格局如下
{ format, // 对应格式化的后果 days, // 天数 hours, // 小时 minutes, // 分钟 seconds, // 秒数 milliseconds, // 毫秒 }
具体实现
基本思路
- 依据 传入工夫
time
派生出 剩余时间remain
,并计算出对应的 完结工夫endTime
- 通过 requestAnimationFrame + 递归 的形式更新
remain
值,即remain = endTime - Date.now()
-
依据最新的
remain
值,通过parseTime()
和formatTime()
办法进行转换返回对应的后果parseTime()
负责将remain
值转换成 天数 / 小时 / 分钟 / 秒 / 毫秒 等值formatTime()
负责将输入后果格式化,例如 有余位补 0 等
成果展现
具体代码
src\components\CountDown\index.vue
<template>
<div class="count-down">
<slot v-bind="currentTime">
<h1>{{currentTime.format}}</h1>
</slot>
</div>
</template>
<script setup>
import {computed, ref, onMounted} from 'vue'
import useCountDown from './Composable/useCountDown'
const props = defineProps({
time: {
type: Number,
default: 0,
},
format: {
type: String,
default: 'DD:HH:mm:ss:SSS',
},
immediate: {
type: Boolean,
default: true,
},
})
const emits = defineEmits(['finish'])
const {start, currentTime} = useCountDown({
...props,
onFinish: () => emits('finish'),
})
// 判断是否须要立刻执行
onMounted(() => {if (props.immediate) start()})
// 向内部裸露的内容
defineExpose({
start,
currentTime,
})
</script>
src\components\CountDown\composable\useCountDown\index.ts
import {computed, ref} from 'vue'
import {parseTime, formatTime} from '../../utils'
export default (options) => {
// 是否正在倒计时
let counting = false
// 剩余时间
const remain = ref(options.time)
// 完结工夫
const endTime = ref(0)
// 格式化输入的日期工夫
const currentTime = computed(() => formatTime(options.format, parseTime(remain.value)))
// 获取以后剩余时间
const getCurrentRemain = () => Math.max(endTime.value - Date.now(), 0)
// 设置剩余时间
const setRemain = (value) => {
// 更新剩余时间
remain.value = value
// 倒计时完结
if (value === 0) {
// 触发 Finish 事件
options.onFinish?.()
// 正在倒计时标记为 false
counting = false
}
}
// 倒计时
const tickTime = () => {requestAnimationFrame(() => {
// 更新剩余时间
setRemain(getCurrentRemain())
// 倒计时没完结,就持续
if (remain.value > 0) {tickTime()
}
})
}
// 启动
const start = () => {
// 正在倒计时,疏忽屡次调用 start
if (counting) return
// 正在倒计时标记为 true
counting = true
// 设置完结工夫
endTime.value = Date.now() + remain.value
// 开启倒计时
tickTime()}
return {
currentTime,
start
}
}
src\components\CountDown\utils\index.ts
// 常量
const SECOND = 1000
const MINUTE = 60 * SECOND
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
// 解析工夫
export const parseTime = (time) => {const days = Math.floor(time / DAY)
const hours = Math.floor((time % DAY) / HOUR)
const minutes = Math.floor((time % HOUR) / MINUTE)
const seconds = Math.floor((time % MINUTE) / SECOND)
const milliseconds = Math.floor(time % SECOND)
return {
days,
hours,
minutes,
seconds,
milliseconds,
}
}
// 格式化工夫
export const formatTime = (format, time) => {let { days, hours, minutes, seconds, milliseconds} = time
// 判断是否须要展现 天数,须要则补 0,否则将 天数 降级加到 小时 局部
if (format.includes('DD')) {format = format.replace('DD', padZero(days))
} else {hours += days * 24}
// 判断是否须要展现 小时,须要则补 0,否则将 小时 降级加到 分钟 局部
if (format.includes('HH')) {format = format.replace('HH', padZero(hours))
} else {minutes += hours * 60}
// 判断是否须要展现 分钟,须要则补 0,否则将 分钟 降级加到 秒数 局部
if (format.includes('mm')) {format = format.replace('mm', padZero(minutes))
} else {seconds += minutes * 60}
// 判断是否须要展现 秒数,须要则补 0,否则将 秒数 降级加到 毫秒 局部
if (format.includes('ss')) {format = format.replace('ss', padZero(seconds))
} else {milliseconds += seconds * 1000}
// 默认展现 3 位 毫秒数
if (format.includes('SSS')) {const ms = padZero(milliseconds, 3)
format = format.replace('SSS', ms)
}
// 最终返回格式化的数据
return {format, days, hours, minutes, seconds, milliseconds}
}
// 有余位数用 0 填充
export const padZero = (str, padLength = 2) => {
str += ''
if (str.length < padLength) {str = '0'.repeat(padLength - str.length) + str
}
return str
}
最初
欢送关注同名公众号《
熊的猫
》,文章会同步更新,也可疾速退出前端交换群!
以上就是一个根本的计时器的实现了,其中必定有不足之处,不过大家只须要抓住核心思想即可,很多内容都借鉴了 vant-count-down 组件 的实现,感兴趣能够间接去看其对应的源码。
心愿本文对你有所帮忙!!!