今天整理了一下 JavaScript 定时器,顺便了解了一下 JavaScript 的运行机制,现在记录一下。
JavaScript 执行机制
浏览器( JavaScript 引擎)执行 JavaScript 的机制是基于事件循环的。由于 JavaScript 是单线程,同一时间只能执行一个任务。为了避免某些长时间任务造成无意义等待,JavaScript 引入了异步概念。
同步任务直接在主线程队列中顺序执行,而异步任务会进入另一个任务队列,不会阻塞主线程。等到主线程队列空了(执行完了)的时候,就会去异步队列查询是否有可执行的异步任务了(异步任务通常进入异步队列之后还要等一些条件才能执行,如 ajax 请求、文件读写),如果某个异步任务可以执行了便加入主线程队列,以此循环。
JavaScript 定时器
定时器也是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间就由定时器模块来管理,当某个定时器到了可执行状态,就会被加入主线程队列。
了解了上面的执行机制,我们不难理解 JavaScript 定时器不是绝对精准的,延迟的时间严格来说总是大于我们设定的时间的,至于大多少就要看当时 JavaScript 的执行情况了。
另外,多个定时器如不及时清除(clearTimeout()
),会造成干扰,使延迟时间更加捉摸不透。所以,不管定时器有没有执行完,要及时清除不需要的定时器。
下面来介绍几个常用的 JavaScript 定时器:
setTimeout()
设置一个定时器,在定时器到期后执行一次函数或代码段:
setTimeout(fn, x)
表示延迟 x
毫秒之后执行 fn
。
var timeoutId = setTimeout(code, delay, param1, param2, ...) var timeoutId = setTimeout(func, delay, param1, param2, ...)
timeoutId
定时器ID
func
延迟后执行的函数
code
延迟后执行的代码字符串,不推荐使用原理类似eval()
delay
延迟的时间(单位:毫秒),默认值为0
param1,param2
向延迟函数传递而外的参数,IE9以上支持
HTML5 规范规定最小延迟时间不能小于 4ms ,即 x
如果小于 4 ,会被当做 4 来处理。 不过不同浏览器的实现不一样,比如,Chrome可以设置1ms,IE11/Edge是4ms。
另外, setTimeout()
方法不是 Ecmascript 规范定义的内容,而是属于BOM提供的功能。setTimeout()
延迟也是有上限的,如果大于 2 的 31 次方的话,会立即执行,延迟无效。
setInterval()
以固定的时间间隔重复调用一个函数或者代码段:
var intervalId = window.setInterval(func, delay , param1, param2, ...]); var intervalId = window.setInterval(code, delay);
intervalId
重复操作的ID
func
延迟调用的函数
code
代码段
delay
延迟时间,没有默认值
setInterval
的实现机制跟 setTimeout
类似,只不过是重复执行的。
对于 setInterval(fn, 100)
容易产生一个误区:并不是上一次 fn
执行完了之后再过 100ms 才开始执行下一次 fn
。 事实上,setInterval
并不管上一次 fn
的执行结果,而是每隔 100ms 就将 fn
放入主线程队列,而两次 fn
之间具体间隔多久就不一定了,跟 setTimeout
实际延迟时间类似,和 JavaScript 执行情况有关。
(function testSetInterval() { let i = 0; const start = Date.now(); const timer = setInterval(() => { i += 1; i === 5 && clearInterval(timer); console.log(`第${i}次开始`, Date.now() - start); for(let i = 0; i < 100000000; i++) {} console.log(`第${i}次结束`, Date.now() - start); }, 100); })(); /*输出 第1次开始 100 第1次结束 1089 第2次开始 1091 第2次结束 1396 第3次开始 1396 第3次结束 1701 第4次开始 1701 第4次结束 2004 第5次开始 2004 第5次结束 2307 */
setImmediate()
在浏览器完全结束当前运行的操作之后立即执行指定的函数(仅 IE10 和 Node 0.10+ 中有实现),类似 setTimeout(func, 0)
。
var immediateId = setImmediate(func , param1, param2, ...]); var immediateId = setImmediate(func);
immediateId
定时器ID
func
回调
这算一个比较新的定时器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他浏览器未测试。
从API名字来看很容易联想到setTimeout(0),不过setImmediate应该算是setTimeout(0)的替代版。
在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。不过在Nodejs中,两者谁先执行都有可能,原因是Nodejs的事件循环和浏览器的略有差异。
(function testSetImmediate() { const label = 'setImmediate'; console.time(label); setImmediate(() => { console.timeEnd(label); }); })(); /* Edge输出:setImmediate: 0.555 毫秒 */
很明显,setImmediate设计来是为保证让代码在下一次事件循环执行,以前setTimeout(0)这种不可靠的方式可以丢掉了。
requestAnimationFrame()
专门为实现高性能的帧动画而设计的API,但是不能指定延迟时间,而是根据浏览器的刷新频率(帧)而定。
var requestId = window.requestAnimationFrame(func);
func
回调
上面简单的介绍了四种 JavaScript 的定时器,而本文将会主要介绍比较常用的两种:setTimeout()
和 setInterval()
。
clearInterval()、clearTimeout() 取消定时器
clearInterval()
方法可取消由 setInterval()
函数设定的定时执行操作。
clearInterval()
方法的参数必须是由 setInterval()
返回的 ID 值。
myVar = setInterval(func, 200); // 设置一个定时器 clearInterval(myVar); // 取消这个定时器
myVar
调用 setInterval()
函数时所获得的返回值,使用该返回标识符作为参数,可以取消该 setInterval()
所设定的定时执行操作。
clearTimeout() 同上。
举例:
基本用法:
// 下面代码执行之后会输出什么? var intervalId, timeoutId; timeoutId = setTimeout(function () { console.log(1); }, 300); setTimeout(function () { clearTimeout(timeoutId); console.log(2); }, 100); setTimeout('console.log("5")', 400); intervalId = setInterval(function () { console.log(4); clearInterval(intervalId); }, 200); // 分别输出: 2、4、5
setInterval
和 setTimeout
的区别:
// 执行在面的代码块会输出什么? setTimeout(function () { console.log('timeout'); }, 1000); setInterval(function () { console.log('interval') }, 1000); // 输出一次 timeout,每隔1S输出一次 interval /*--------------------------------*/ // 通过setTimeout模拟setInterval 和 setInterval有啥区别么? var callback = function () { if (times++ > max) { clearTimeout(timeoutId); clearInterval(intervalId); } console.log('start', Date.now() - start); for (var i = 0; i < 990000000; i++) {} console.log('end', Date.now() - start); }, delay = 100, times = 0, max = 5, start = Date.now(), intervalId, timeoutId; function imitateInterval(fn, delay) { timeoutId = setTimeout(function () { fn(); if (times <= max) { imitateInterval(fn ,delay); } }, delay); } imitateInterval(callback, delay); intervalId = setInterval(callback, delay);
如果是setTimeout
和setInterval
的话,它俩仅仅在执行次数上有区别,setTimeout
一次、setInterval
n次。
而通过setTimeout
模拟的setInterval
与setInterval
的区别则在于:setTimeout
只有在回调完成之后才会去调用下一次定时器,而setInterval
则不管回调函数的执行情况,当到达规定时间就会在事件队列中插入一个执行回调的事件,所以在选择定时器的方式时需要考虑setInterval
的这种特性是否会对你的业务代码有什么影响?
setTimeout(func, 0)
和 setImmediate(func)
谁更快?(仅仅是好奇,才写的这段测试)
console.time('immediate'); console.time('timeout'); setImmediate(() => { console.timeEnd('immediate'); }); setTimeout(() => { console.timeEnd('timeout'); }, 0); }, 0);
在Node.JS v6.7.0
中测试发现setTimeout
更早执行。