资讯专栏INFORMATION COLUMN

从倒计时的实现深入探究setTimout与setInterval

Nekron / 3350人阅读

摘要:这么执行导致的结果是每次的时间必然会大于主线程代码执行消耗的时间,而当这个主线程代码执行消耗的时间累加起来超过时,就会出现跳一秒的情况。

拜年

新年伊始,本搬砖汪先给各位老爷们拜个晚年,祝各位技术大牛们在新的一年代码功底更进一步,家庭幸福美满!

需求

下面进入正题:在翻阅segmentfault社区时看到某巨厂面试要求实现一个倒计时功能,之前也没有仔细实现过,趁年初来任务还没来得及分配,赶紧着手实现了一个。

第一版
var period = 60*1000*60*2
var end = new Date().getTime() + period
var date = new Date(end)
var interval = 1000
var count = 0
var startTime = new Date().getTime()

console.log("开始时间:" + startTime)

function loopInner() {
  count++

  var diff = end - new Date().getTime()
  var h = Math.floor(diff / (60*1000*60))
  var hdiff = diff % (60*1000*60)
  var m = Math.floor(hdiff / (60*1000))
  var mdiff = hdiff % (60*1000)
  var s = mdiff / 1000
  var sCeil = Math.ceil(s)

  var j = 0
  while (j<100000000) { // 放大主线程代码执行时间
    j++
  }

  console.log(h + "小时", m + "分钟:", s + "秒(精确到毫秒)", sCeil + "秒(进一法)")
}

function loop() {
  loopInner() // 首先var j = 0

  if (count === 100) {
    var endTime = new Date().getTime()
    console.log("结束时间:" + endTime) // 打印开始时间
    console.log("时间差毫秒数:" + Number(endTime - startTime) + "对应秒数:" + Number(endTime - startTime) / 1000)
    console.log("计时器计算秒数:100")
  } else {
    return setTimeout(loop, interval)
  }
}

loop()

结果如下:


第一版实现我使用的是递归的setTimeout方法,原因是之前曾经看到过递归的setTimeout能避免setInterval忽视代码执行时间,而一个事件队列里只会有一个setInterval事件导致的部分setInterval事件被忽略的情况。这么执行导致的结果是每次setTimeout的时间必然会大于1000ms(1000 + 主线程代码执行消耗的时间),而当这个主线程代码执行消耗的时间累加起来超过1s时,就会出现跳一秒的情况。这一版实现方案的结果不尽如人意。

第二版
var period = 60 * 1000 * 60 * 2
var end
var date = new Date(end)
var interval = 1000
var count = 0
var startTime = new Date().getTime()

console.log("开始时间:" + startTime)

var loop = function () {
  count++
  if (count === 100) {
    var endTime = new Date().getTime()
    console.log("结束时间:" + endTime) // 打印开始时间
    console.log("时间差毫秒数:" + Number(endTime - startTime) + "对应秒数:" + Number(endTime - startTime) / 1000)
    console.log("计时器计算秒数:100")
    return clearInterval(Itvid)
  }

  if (!end) { end = new Date().getTime() + period }
  var diff = end - new Date().getTime()
  var h = Math.floor(diff / (60 * 1000 * 60))
  var hdiff = diff % (60 * 1000 * 60)
  var m = Math.floor(hdiff / (60 * 1000))
  var mdiff = hdiff % (60 * 1000)
  var s = mdiff / (1000)
  var roundS = Math.round(s)

  var j = 0
  while (j<100000000) { // 放大主线程代码执行时间
    j++
  }

  console.log(h + "小时:", m + "分钟:", s + "秒(精确到毫秒)", roundS + "秒(四舍五入)")
}
var Itvid = setInterval(loop, interval)

结果如下:


这一版的结果比较接近正确答案,利用setInterval不等待执行代码完成就直接加入队列的特性(参考setInterval与setTimeout的精确度问题),再加上用Math.round方法修正js的异步方法所造成的几毫秒的误差即可。而setInterval毕竟也是浏览器的api,同样是有几毫秒的差异的。

第三版

这一版是我选择在第一种写法的基础上做改良:每次循环中基于此次代码执行所消耗的时间对下次循环所消耗的时间间隔做修正。

var period = 60 * 1000 * 60 * 2
var startTime = new Date().getTime();
var count = 0
var end = new Date().getTime() + period
var interval = 1000
var currentInterval = interval

console.log("开始时间:" + startTime) // 打印开始时间

function loop() {
  count++
  var offset = new Date().getTime() - (startTime + count * interval); // 代码执行所消耗的时间
  var diff = end - new Date().getTime()
  var h = Math.floor(diff / (60 * 1000 * 60))
  var hdiff = diff % (60 * 1000 * 60)
  var m = Math.floor(hdiff / (60 * 1000))
  var mdiff = hdiff % (60 * 1000)
  var s = mdiff / (1000)
  var sCeil = Math.ceil(s)
  var sFloor = Math.floor(s)
  currentInterval = interval - offset // 得到下一次循环所消耗的时间

  var j = 0
  while (j<100000000) { // 放大主线程代码执行时间
    j++
  }

  console.log("时:"+h, "分:"+m, "毫秒:"+s, "秒向上取整:"+sCeil, "代码执行时间:"+offset+"ms", "下次循环间隔"+currentInterval+"ms") // 打印 时 分 秒 代码执行时间 下次循环间隔
  if (count === 100) {
    var endTime = new Date().getTime()
    console.log("结束时间:" + endTime) // 打印开始时间
    console.log("时间差毫秒数:" + Number(endTime - startTime) + "对应秒数:" + Number(endTime - startTime) / 1000)
    console.log("计时器计算秒数:100")
  } else {
    setTimeout(loop, currentInterval)
  }
}

setTimeout(loop, currentInterval)

结果如下:


暂时性结论

对于同步代码执行耗时不是过大(几十毫秒到几百毫秒之间)的情况,通过实验得到结果:

setInterval > 修正时间间隔的递归setTimeout > 递归setTimeout

疑问

业务场景中是否存在同步代码执行时间超过数秒的情况?

业务场景中实现倒计时的标准做法?

从服务端端获取开始时间会有时间损耗(http传输的耗时),这个耗时有没有方法规避?

依然遗留这些问题存在,还请各位不吝赐教。

参考资料

JS实现活动精确倒计时
w3.org
javascript线程解释(setTimeout,setInterval你不知道的事)

原文

欢迎访问我的博客

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93219.html

相关文章

  • JS 异步实现

    摘要:由于引擎同一时间只执行一段代码这是由单线程的性质决定的,所以每个代码块阻塞了其它异步事件的进行。这意味着浏览器将等待着一个新的异步事件发生。异步的任务执行的顺序是不固定的,主要看返回的速度。 我们经常说JS是单线程的,比如node.js研讨会上大家都说JS的特色之一是单线程的,这样使JS更简单明了,可是大家真的理解所谓JS的单线程机制吗?单线程时,基于事件的异步机制又该当如何,这些知识...

    sihai 评论0 收藏0
  • 浏览器渲染机制

    摘要:浏览器渲染进程浏览器内核进程,内部是多线程的默认每个页面一个进程,互不影响。事件触发线程归属于浏览器而不是引擎,用来控制事件循环可以理解成引擎自己都忙不过来,需要浏览器另开线程协助。 线程和进程 进程和线程的概念可以这样理解: 进程是一个工厂,工厂有它的独立资源--工厂之间相互独立--线程是工厂中的工人,多个工人协作完成任务--工厂内有一个或多个工人--工人之间共享空间 工厂有多个工人...

    appetizerio 评论0 收藏0
  • 浏览器渲染机制

    摘要:浏览器渲染进程浏览器内核进程,内部是多线程的默认每个页面一个进程,互不影响。事件触发线程归属于浏览器而不是引擎,用来控制事件循环可以理解成引擎自己都忙不过来,需要浏览器另开线程协助。 线程和进程 进程和线程的概念可以这样理解: 进程是一个工厂,工厂有它的独立资源--工厂之间相互独立--线程是工厂中的工人,多个工人协作完成任务--工厂内有一个或多个工人--工人之间共享空间 工厂有多个工人...

    lncwwn 评论0 收藏0
  • HTML执行顺序-一探究

    摘要:而进程是多线程的,它主要包含以下主要线程渲染线程负责渲染浏览器界面,解析,,构建树和树,布局和绘制等。且加载解析执行会阻止解析器往下执行,要强调渲染和下载是不冲突的,渲染是线程在执行,下载是下载线程在执行,浏览器多线程。 了解浏览器线程基础 一个页面的呈现主要是由浏览器渲染进程实现的(render进程),主要作用为页面的渲染,脚本执行,事件处理等。而render进程是多线程的,它主要包...

    darry 评论0 收藏0
  • 浏览器知识

    摘要:浏览器的渲染进程是多线程的。异步请求线程在在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。 [TOC] 浏览器进程线程 区分线程和进程 **- 什么是进程** 狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being exe...

    Pluser 评论0 收藏0

发表评论

0条评论

Nekron

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<