JavaScript - 定时器

setTimeout() 和 setInterval() 可以用来实现延时 定时等功能, 但是 JavaScript 是单线程工作, 不能同时执行多个操作 , 只能按照队列先后顺序执行, 定时器仅仅是计划代码在未来执行什么时间执行, 但是这个时间是不能保证的(不能保证定时器延迟), 是不精确的.

# timer 工作原理

(https://johnresig.com/blog/how-javascript-timers-work/) Timers.png

首先,在JavaScript的第一个块中,启动两个定时器:10ms setTimeout和10ms setInterval。由于定时器启动的位置和时间,它实际上在我们实际完成第一个代码块之前触发。但请注意,它不会立即执行(由于线程,它无法执行此操作)。相反,延迟功能排队,以便在下一个可用时刻执行。

此外,在第一个JavaScript块中,我们看到鼠标单击。与此异步事件关联的JavaScript回调(我们永远不知道用户何时可以执行操作,因此它被认为是异步的)无法立即执行,因此,与初始计时器一样,它排队等待稍后执行。

在JavaScript的初始块完成后,执行浏览器会立即询问:什么等待执行?在这种情况下,鼠标单击处理程序和计时器回调都在等待。然后浏览器选择一个(鼠标单击回调)并立即执行它。计时器将等到下一个可能的时间,以便执行。

请注意,当鼠标单击处理程序执行时,执行第一个间隔回调。与计时器一样,它的处理程序排队等待以后执行。但是,请注意,当再次触发间隔时(执行计时器处理程序时),此时将删除处理程序执行。如果要在执行大块代码时排队所有间隔回调,结果将是一堆间隔执行时,它们之间没有延迟,完成后。相反,浏览器倾向于等待,直到排队更多时,不再有间隔处理程序排队(对于所讨论的间隔)。

事实上,我们可以看到,当间隔本身正在执行时,第三个间隔回调会触发。这向我们展示了一个重要的事实:Intervals不关心当前正在执行的内容,它们将不加选择地排队,即使这意味着将牺牲回调之间的时间。

最后,在第二个间隔回调完成执行后,我们可以看到JavaScript引擎没有任何内容可以执行。这意味着浏览器现在等待发生新的异步事件。当间隔再次发射时,我们得到50ms的标记。然而,这次没有任何东西阻止它的执行,所以它会立即触发。

# setTimeout() 和 setInterval()

1
2
3
var id = setTimeout(fn, delay); // 启动单个定时器,在延迟后调用指定的功能。该函数返回一个唯一的ID,可以在以后取消定时器。
var id = setInterval(fn, delay); // 类似setTimeout但不断地调用函数(每次都有延迟),直到它被取消。
clearInterval(id);clearTimeout(id); // 接受计时器ID(由上述任一功能返回)并停止发生计时器回调。
1
2
3
4
5
6
7
8
setTimeout(function(){
  /* Some long block of code... */
  setTimeout(arguments.callee, 10); // setTimeout 链式调用
}, 10); 
 
setInterval(function(){
  /* Some long block of code... */
}, 10);

这两段代码乍一看似乎在功能上等同,但它们不是。值得注意的是,setTimeout代码在前一次回调执行后总是至少有10ms的延迟(它可能最终会更多,但永远不会更少, setTimeout 链式调用是为了防止定时器 setInterval() 因为延迟而可能出现的两个缺点: 1) 某些间隔会被跳过 2) 多个定时器的代码执行之间的间隔可能会比预期小 ),而setInterval无论最后一次回调何时被执行,它都会尝试每10ms执行一次回调。

# 如何得到相对精确的倒计时(秒杀活动, 不同的人得到的时间不一致问题)

不使用客户端的时间, 使用服务器的时间, 但是使用服务器时间也有传输时间消耗和渲染时间消耗, 所以不能做到绝对精确, 但是可以通过优化来达到相对的精确, 控制误差.

当前服务器时间 = 服务器系统返回时间 + 网络传输时间 + 前端渲染时间

通过计算获得服务器时间后到前端渲染完成的时间差, 来动态调整定时器的间隔来缩小误差.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//线程占用
setInterval(function(){ 
  var j = 0; 
  while(j++ < 100000000); 
}, 0); 
 
//倒计时
var interval = 1000,
    ms = 50000,  //从服务器和活动开始时间计算出的时间差,这里测试用50000ms
    count = 0,
    startTime = new Date().getTime();
if( ms >= 0){
  var timeCounter = setTimeout(countDownStart,interval);                  
}
 
function countDownStart(){
  count++;
  var offset = new Date().getTime() - (startTime + count * interval);
  var nextTime = interval - offset;
  var daytohour = 0; 
  if (nextTime < 0) { nextTime = 0 };
  ms -= interval;
  console.log("误差:" + offset + "ms,下一次执行:" + nextTime + "ms后,离活动开始还有:" + ms + "ms");
  if(ms < 0){
    clearTimeout(timeCounter);
  }else{
    timeCounter = setTimeout(countDownStart,nextTime);
  }
}
思路来源: http://imgcache.gtimg.cn/club/common/lib/zero/widgets/date/Date.1.1.1.js
Licensed under CC BY-NC-SA 4.0