这几天用到了 setinterval(),但是发现 setInterval 有缺点,都建议用 setTimeout 模拟 setInterval,也可理解为链式的 setTimeout。所以这里总结一下settimeout()和setinterval()的区别,以及它们之间的相互模拟。
一、setTimeout 和 setInterval 的区别
setTimeout() 定义和用法:
定义:
setTimeout()方法用于在指定毫秒数后再调用函数或者计算表达式(以毫秒为单位)
语法:
setTimeout(code,millisec)
code:必需,要调用的函数后要执行的 JavaScript 代码串;millisec:必需,在执行代码前需等待的毫秒数。
setTimeout() 只执行函数一次,如果需要多次调用可以使用 setInterval(),或者在函数体内再次调用setTimeout()
示例代码:延迟1秒弹出 Hello
// 延迟1秒弹出 Hello setTimeout(function(){ alert("Hello"); }, 1000);
setInterval() 定义和用法:
定义:
setInterval() 方法用于按照指定的周期(以毫秒计)来循环调用函数或计算表达式,直到 clearInterval() 被调用或窗口关闭,由 setInterval() 返回的 ID 值可用作 clearInterval() 方法的参数。
语法:
setInterval(code,millisec[,"lang"])
code:必需,要调用的函数或要执行的JavaScript 代码串;millisec:必须,周期性执行或调用 code 之间的时间间隔,以毫秒计。
setInterval() 会不停的调用函数,直到clearInterval() 被调用或者窗口被关闭,由 setInterval() 返回的ID值可用作 clearInterval() 方法的参数。
示例代码:一直显示当前时间,点击停止不继续
<p>显示当前时间:</p> <p id="demo"></p> <button onclick="myStopFunction()">停止时间</button> <!-- 一直显示当前时间,点击停止不继续 --> <script> var myVar = setInterval(function(){ myTimer() }, 1000); function myTimer() { var d = new Date(); var t = d.toLocaleTimeString(); document.getElementById("demo").innerHTML = t; } function myStopFunction() { clearInterval(myVar); } </script>
区别总结:
setTimeout() 方法只运行一次,也就是说当达到设定的时间后就开始运行指定的代码,运行完后就结束了,次数是一次。
setInterval() 是循环执行的,即每达到指定的时间间隔就执行相应的函数或者表达式,只要窗口不关闭或 clearInterval() 调用就会无限循环下去。
二、知识储备
1、浏览器
是个多进程
应用
首先你要知道 浏览器是个多进程应用,那这些进程里都包含哪些:
- Browser进程:浏览器的主进程(负责协调、主控),只有一个,作用:
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程:最多一个,用于3D绘制等
- 浏览器内核(浏览器渲染进程,它内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
2、浏览器内核
中的多线程
- GUI渲染线程
- JS引擎线程:负责解析Javascript脚本,运行代码。
- 事件触发线程:归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。
由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行) - 定时触发器线程:因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确。计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。
- 异步http请求线程
三、setInterval缺点
定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。
setInterval(function, N) //即:每隔N秒把function事件推到消息队列中
上图可见,setInterval每隔100ms往队列中添加一个事件;100ms后,添加T1定时器代码至队列中,主线程中还有任务在执行,所以等待,some event执行结束后执行T1定时器代码;又过了100ms,T2定时器被添加到队列中,主线程还在执行T1代码,所以等待;又过了100ms,理论上又要往队列里推一个定时器代码,但由于此时T2还在队列中,所以T3不会被添加
,结果就是此时被跳过
;这里我们可以看到,T1定时器执行结束后马上执行了T2代码
,所以并没有达到定时器的效果。
综上所述,setInterval有两个缺点:
使用setInterval时,某些间隔会被跳过
;即使setInterval调用的方法报错了,他仍然会继续执行。无视网络延迟,可能多个定时器会连续执行
;
可以这么理解:每个setTimeout产生的任务会直接push到任务队列中;而setInterval在每次把任务push到任务队列前,都要进行一下判断(看上次的任务是否仍在队列中)
。
因而我们一般用 setTimeout 模拟 setInterval,来规避掉上面的缺点。
模拟 setInterval() :
将 setTimeout() 包含在一个执行函数A中,而setTimeout() 自己的code执行函数又是A,然后在函数A外将函数A执行一次,即达到了循环执行的目的。
setTimeout(function () { // 任务 setTimeout(arguments.callee, 1000); }, 1000)
上述函数每次执行的时候都会创建一个新的定时器,第二个 setTimeout 使用了 arguments.callee() 获取当前函数的引用,并且为其设置另一个定时器。
好处:
在前一个定时器执行完前,不会向队列插入新的定时器
(解决缺点一)- 保证定时器间隔(解决缺点二)
示例代码:
var intervalNum = 0; function testsetInterval() { var date = new Date(); console.log(date.getSeconds()); console.log("setInterval", intervalNum++); } function recursive() { testsetInterval(); setTimeout(function () { recursive() //递归,每隔4秒调用一次recursive() }, 4000) } function testFuntion() { recursive(); //在方法recursive外,调用一次recursive,以启动循环调用! }
实现循环执行,和 setInterval() 功能相同。
当然也可以用 setInterval() 来模拟 setTimeout() ,具体使用那个,以具体的需求和场景具体分析,就像for循环可以模拟所有的循环一样(包括分支,以及do while一样)。一般情况下 setTimeout() 用于延迟执行某方法或功能;setInterval() 则一般用于刷新表单,对于一些表单的假实时指定时间刷新同步。
模拟 setTimeout() :
用 setInterval() 模拟 setTimeout() 很简单,在 setInterval() 执行一次后,立刻关闭窗口(当然这是耍无赖)或者执行 clearInterval() 方法(这个靠谱点)。clearInterval() 需要在 setInterval()执行code方法内或其他地方执行,不能紧接着 setInterval() 后面执行,那样setInterval() 还没等到执行,就已经被干掉了。
示例代码:
var intervalNum = 0, clearId = 0; function testsetInterval(){ var date = new Date(); console.log(date.getSeconds()); console.log("setInterval", intervalNum++); clearInterval(clearId); //也可以在此执行 } function testFuntion() { clearId = setInterval(function () { testsetInterval(); //每隔4秒调用testsetInterval() // clearInterval(clearId); //可以在此执行 },4000); }
执行一次,关闭 setInterval(),和 setTimeout() 功能相同
最后,解释为什么 “ 建议传入函数而不是字符串以作为第一个参数”。
setTimeout()、setInterval() 允许传入一个JS代码字符串并执行,然而在JS代码中执行另一段JS代码时,代码首先会以正常的方式求值,然后在执行过程中对包含于字符串中的代码发起另一个求值运算,从而造成双重求值。它比直接包含的代码执行速度慢很多,原因在于, 每次调用setTimeout()、setInterval() 都会创建一个新的解释器/编译器实例。这必然使得代码执行速度变慢,效率降低,从而造成性能的浪费。所以建议传入函数而不是字符串来作为第一个参数。
实例
下面写了一个小例子:点击 start 开始旋转,点击 stop 停止旋转。
先是用 setInterval() 实现的:
<div class="divOne"> 123 </div> <button id="click">start</button> <button id="stop">stop</button> <script src="./js/jquery-1.8.3.min.js"></script> <script src="./js/jquery.rotate.min.js"></script> <script> var angle = 0; var myVar; $('#click').on('click', function(){ myVar = setInterval(function(){ myTimer() }, 10); }) function myTimer() { angle += 3; $('.divOne').rotate(angle); } $("#stop").on('click', function(){ clearInterval(myVar) }) </script>
然后用 setTimeout 来模拟 setInterval 实现:
var angle = 0; $('#click').on('click', function(){ setTimeout(myTimer, 10) }) var myTimer = function() { setTimeout(myTimer, 10) angle += 3; $('.divOne').rotate(angle); } $("#stop").on('click', function(){ console.log(111) myTimer = null; }) // 或者 var angle = 0; var stop = false; $('#click').on('click', function(){ stop = false; setTimeout(function () { angle += 3; $('.divOne').rotate(angle); var myTimer = setTimeout(arguments.callee, 10); if( stop ){//符合条件 删除定时器 clearTimeout(myTimer); } }, 10); }) $("#stop").on('click', function(){ console.log(111) stop = true; })
可以实现同样的功能。