乐趣区

关于前端:面试官你先实现个-CountDown-计时器组件吧

前言

欢送关注同名公众号《熊的猫》,文章会同步更新,也可疾速退出前端交换群!

最近有个同学在面试时被要求手写一个 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 的超时,并可能齐全卸载它们
    • 例如,若标签中蕴含 AudioContextFirefox 不会对非流动标签进行节流
  • 追踪型脚本的节流

    • 例如,Firefox 对它辨认为追踪型脚本的脚本 施行额定节流 ,即当在 前台运行 时, 节流的最小提早是 4ms
    • 当在 后盾标签 中, 节流的最小提早是 10000ms(即 10s),在文档首次加载后 30s 开始失效
  • 在加载页面时推延超时

    • 以后标签页正在加载时,Firefox 将推延触发 setTimeout() 计时器,直到主线程被认为是闲暇 的(相似于 window.requestIdleCallback())或 直到 加载事件触发结束,才开始触发

requestAnimationFrame

是什么?

window.requestAnimationFrame()  会通知浏览器咱们心愿执行一个 动画 ,并且要求浏览器在下次 重绘之前 调用指定的回调函数 更新动画,即每 16.67ms 执行一次回调函数。

回调函数的参数

回调办法在会接管到一个 DOMHighResTimeStamp 参数,它是一个 十进制数,单位为毫秒,最小精度为 1ms(1000μs)

同一帧 中的 多个回调函数 都会承受到一个 雷同的工夫戳 ,即便在计算上一个回调函数的工作负载期间曾经耗费了一些工夫,因而要确保总是应用 第一个参数(或其余一些获取以后工夫的办法) 来计算动画在一帧中的进度,否则动画在 高刷新率 的屏幕中会 运行得更快

暂停调用

为了进步性能和电池寿命,在大多数浏览器里,当 requestAnimationFrame() 运行在 后盾标签页 暗藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以晋升性能和电池寿命。

到底选谁?

从以上内容来看,仿佛没有一个完满的计划呀,这不是更加大难度了?

莫慌!既然都不完满,那么也要从矮个子中挑个高个子。

先不思考别的,setIntervalsetTimeout 有一个致命的毛病:

  • 最大延时限度

    • 延时工夫一旦大于 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 组件 的实现,感兴趣能够间接去看其对应的源码。

心愿本文对你有所帮忙!!!

退出移动版