面试官:你先实现个 CountDown 计时器组件吧!

前言

欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!

最近有个同学在面试时被要求手写一个 CountDown 计时器组件,但可能是因为之前没有了解过,所以思路上没有那么顺畅,过后他询问我应该怎么写(哈哈,我也没写过),于是就有了本篇文章,希望本篇文章对你有所帮助!!!

面试官:你先实现个 CountDown 计时器组件吧!_第1张图片

不难看出,要求手写一个 CountDown 计时器组件 目的无非考察如下几个方面(谁也不知道面试官在想什么):

  • 组件封装能力

    • 组件输入,即对应组件 内部 Props 的 设计 和 考量
    • 组件输出,即对应组件 对外提供 的 属性 或 方法
    • 逻辑复用,即指组件内部逻辑的 可组合性
  • 时间相关的敏感度

    • 倒计时的实现方式有多种,例如 setInterval、setTimeout、requestAnimationFrame 等等,那么哪种更合适?
    • 获取当前时间可以用 Date.now()、performance.now(),那么该怎么选?

setInterval & setTimeout & requestAnimationFrame

倒计时功能必然需要一个不断执行的 异步过程没疑问吧),这可以使用运行时环境提供的 API,即 setInterval、setTimeout、requestAnimationFrame,那么到底该选择谁更合适呢?

面试官:你先实现个 CountDown 计时器组件吧!_第2张图片

下面进行逐个分析!

setInterval

是什么?

setInterval() 方法会重复调用 一个函数执行一个代码片段,在每次调用之间具有固定的时间间隔,并会返回一个 interval ID 用于标识唯一的时间间隔,可通过调用 clearInterval()") 来移除定时器。

共享同一个 ID 池

值得注意的是,setInterval() 和 setTimeout() 是 共享同一个 ID 池 的,所以说 clearInterval() 和 clearTimeout()") 在技术上是可 互换使用 的:


    

面试官:你先实现个 CountDown 计时器组件吧!_第3张图片

但为了 避免代码杂乱无章保证代码的可维护性,还是更推荐使用相互匹配的 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() 运行在 后台标签页隐藏的