setTimeout和setInterval的注意事项
精准问题
setTimeout的问题在于它并不是精准的,例如使用setTimeout设定一个任务在10ms后执行,但是在9ms后,有一个任务占用了5ms的cpu时间片,再次轮到定时器执行时,时间已经过期了4ms,那么是不是说setInterval就是准确的呢?
然而并不是,setInterval存在两个问题:
-
时间间隔可能会跳过
-
时间间隔可能小于定时器设定的时间
请看以下代码:
function click() { // code block1... setInterval(function() { // process ... }, 200); // code block2 }
我们假设通过一个click, 触发了setInterval以实现每隔一个时间段执行process代码
在205ms时执行setInterval, 以此为一个时间点, 在205ms时插入process代码, process代码开始执行, 然而process代码执行的时间超过了接下来一个插入时间点405ms, 这样代码队列后又插入了一份process代码, process继续执行着, 而且超过了605ms这个插入时间点
下面问题来了, 由于代码队列中已经有了一份未执行的process代码(405m时插入的), 所以605ms这个插入时间点将会被跳过, 因为js引擎只允许有一份未执行的process代码
为了避免这种情况可以使用setTimeout递归调用
代码如下:
setTimeout(function(){ // processing setTimeout(arguments.callee, interval); }, interval);
每次函数执行的时候都会创建一个新的定时器,第二个setTimeout调用使用了arguments.callee来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样做是为了在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔,也保证了在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。
参数问题
window.setTimeout(expression,milliseconds); window.setInterval(expression,milliseconds);
其中,expression可以是用引号括起来的一段代码,也可以是一个函数名,到了指定的时间,系统便会自动调用该函数,当使用函数名作为调用句柄时,不能带有任何参数;而使用字符串时,则可以在其中写入要传递的参数。两个方法的第二个参数是milliseconds,表示延时或者重复执行的毫秒数。
注意:如果第一个参数使用函数名,到了指定的时间,系统便会自动调用该函数,但函数不能带有任何参数,而且只能给出函数的名。
如果函数给出参数,这将使sum函数立即执行,并将返回值作为调用句柄传递给setInterval函数,其结果并不是程序需要的。
使用字符串形式可以达到想要的结果:
但这种写法不够直观,而且有些场合必须使用函数名,下面用一个小技巧来实现带参数函数的调用:
<script type="text/javascript"> var userName="jack"; //根据用户名显示欢迎信息 function hello(_name){ alert("hello,"+_name); } //创建一个函数,用于返回一个无参数函数 function _hello(_name){ return function(){ hello(_name); } } window.setTimeout(_hello(userName),3000); </script>
这里定义了一个函数_hello,用于接收一个参数,并返回一个不带参数的函数,在这个函数内部使用了外部函数的参数,从而对其调用,不需要使用参数。在window.setTimeout函数中,使用_hello(userName)来返回一个不带参数的函数句柄,从而实现了参数传递的功能。
关于this
在Javascript里,setTimeout和setInterval接收第一个参数是一个字符串或者一个函数,当在一个对象里面用setTimeout延时调用该对象的方法时
function obj() { this.fn = function() { alert("ok"); console.log(this); setTimeout(this.fn, 1000);//直接使用this引用当前对象 } } var o = new obj(); o.fn();
这代码的目的是想让它不断输出“OK”“obj{}”,然后我们发现上面的代码不是想要的结果,原因是setTimeout里面的this是指向window,所以要调用的函数变成 window.fn 为undefined。所以问题的关键在于得到当前对象的引用,于是有以下三种方法
方法一
function obj() { this.fn = function() { alert("ok"); console.log(this); setTimeout(this.fn.bind(this), 1000);//通过Function.prototype.bind 绑定当前对象 } } var o = new obj(); o.fn();
这样可以得到正确的结果,可惜Function.prototype.bind方法是ES5新增的标准,测试了IE系列发现IE6-8都不支持,只有IE9+可以使用。
关于bind的用法:https://msdn.microsoft.com/zh-cn/library/ff841995
要想兼容就得简单的模拟下bind,看下面的代码
方法二
function obj() { this.fn = function() { alert("ok"); setTimeout((function(a,b){ return function(){ b.call(a); } })(this,this.fn), 1000);//模拟Function.prototype.bind } } var o = new obj(); o.fn();
首先通过一个自执行匿名函数传当前对象和对象方法进去,也就是里面的参数a和b,再返回一个闭包,通过call方法使this指向正确。
方法三
function obj() { this.fn = function() { var that = this;//保存当前对象this alert("ok"); setTimeout(function(){ that.fn(); }, 1000);//通过闭包得到当前作用域,好访问保存好的对象that } } var o = new obj(); o.fn();
两个关键点是 保存当前对象this为别名that 和 通过闭包得到当前作用域,以访问保存好的对象that;当对象方法里面多层嵌套函数或者setTimeout,setInterval等方法丢失this(也就是this不指向当前对象而是window),所以在this指向正确的作用域保存var that = this就变得很实用了
-20170321
// 下面代码执行之后会输出什么? 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
JS定时器的工作原理
上图中,左侧数字代表时间,单位毫秒;左侧文字代表某一个操作完成后,浏览器去询问当前队列中存在哪些正在等待执行的操作;蓝色方块表示正在执行的代码块;右侧文字代表在代码运行过程中,出现哪些异步事件。该图大致流程如下:
- 程序开始时,有一个JS代码块开始执行,执行时长约为18ms,在执行过程中有3个异步事件触发,其中包括一个
setTimeout
、鼠标点击事件、setInterval
- 第一个
setTimeout
先运行,延迟时间为10ms,稍后鼠标事件出现,浏览器在事件队列中插入点击的回调函数,稍后setInterval
运行,10ms到达之后,setTimeout
向事件队列中插入setTimeout
的回调 - 当第一个代码块执行完成后,浏览器查看队列中有哪些事件在等待,他取出排在队列最前面的代码来执行
- 在浏览器处理鼠标点击回调时,
setInterval
再次检查到到达延迟时间,他将再次向事件队列中插入一个interval的回调,以后每隔指定的延迟时间之后都会向队列中插入一个回调 - 后面浏览器将在执行完当前队头的代码之后,将再次取出目前队头的事件来执行
需要注意的点
setTimeout
有最小时间间隔限制,HTML5标准为4ms,小于4ms按照4ms处理,但是每个浏览器实现的最小间隔都不同- 因为JS引擎只有一个线程,所以它将会强制异步事件排队执行
- 如果
setInterval
的回调执行时间长于指定的延迟,setInterval
将无间隔的一个接一个执行 this
的指向问题可以通过bind
函数、定义变量、箭头函数的方式来解决
setImmediate
: 在浏览器完全结束当前运行的操作之后立即执行指定的函数(仅IE10和Node 0.10+中有实现),类似setTimeout(func, 0)
var immediateId = setImmediate(func[, param1, param2, ...]); var immediateId = setImmediate(func);
requestAnimationFrame
: 专门为实现高性能的帧动画而设计的API,但是不能指定延迟时间,而是根据浏览器的刷新频率而定(帧)
var requestId = window.requestAnimationFrame(func);
process.nextTick():Node.js是单线程的,除了系统IO之外,在它的事件轮询过程中,同一时间只会处理一个事件。你可以把事件轮询想象成一个大的队列,在每个时间点上,系统只会处理一个事件。即使你的电脑有多个CPU核心,你也无法同时并行的处理多个事件。但也就是这种特性使得node.js适合处理I/O型的应用,不适合那种CPU运算型的应用。在每个I/O型的应用中,你只需要给每一个输入输出定义一个回调函数即可,他们会自动加入到事件轮询的处理队列里。当I/O操作完成后,这个回调函数会被触发。然后系统会继续处理其他的请求。在这种处理模式下,process.nextTick()的意思就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行。
参考:
深入理解定时器:setTimeout与setInterval