这篇文章会聊聊关于Javascript的定时器以及它的执行机制,首先会先翻译一篇比较好的定时器和事件队列的文章,而后会根据一些经典的例题来更深刻了解。javascript
此章节翻译自原文java
在基础层面上去了解 Javascript 定时器的工做原理是蛮重要的。由于单线程的问题不少时候他们的行为都是无心义的。让咱们先来看看构建和操做Timers的函数。浏览器
var id = setTimeout(fn,delay);
初始化一个单次定时器,它会在设置的延迟时间(delay)后执行特定的回调函数。这个函数会返回一个惟一的ID,这个ID能够在以后用来取消这个定时器。var id = setInterval(fn,delay);
跟 setTimeout 相似,可是区别在于这个函数会屡次执行直到它被中止。clearInterval(id);clearTimeout(id);
传入定时器的ID(ID由上面两个函数返回)而后中止定时器回调。为了了解定时器内部的运行原理,有一个很重要的概念咱们须要探讨: 定时器延迟是不能保证的。由于全部的JS脚本都是在浏览器的单个线程上执行,异步事件在被触发的时候才会执行(例如鼠标事件和定时器),这个图能够很好的说清楚。闭包
这张图里面有不少知识点须要消化,可是彻底理解了它就能够更好的帮助咱们了解Javascript的异步事件机制的运做。这个图是一维的:垂直量度标记的是时间,单位是ms,蓝色框里面的是正在执行的Javascript部分。例如第一个Javascript部分执行了大概18ms,鼠标点击事件大概执行了11ms,以此类推。异步
由于Javascript只能一次执行一段代码(由于它的单线程性质),全部的代码块都会在线程里面堵塞其余的代码块。这意味着当异步事件发生的时候(例如鼠标点击事件,定时器触发或者一个XMLHttpRequest完成),它将会进入事件队列排队等待执行(这种排队实际上发生的状况确定会因浏览器和浏览器之间的不一样而有所不一样,因此这里是一个简化的描述)。函数
首先,在第一个Javascript代码块中,两个定时器被启动:10ms的 setTimeout 和 10ms的 setInterval。定时器的启动时间和位置是在咱们完成第一个代码块前就已经触发了。可是请注意,它不会当即执行(因为线程不能执行)。而是进入队列中以便再下一个可用时间执行。【译者补充:就是说会等第一代码块的顺序代码执行完后,回头才会去处理事件队列里面的代码。】学习
另外,在第一个Javascript代码块中咱们还看到一个鼠标点击事件发生。与这个事件关联的异步回调函数也不会立刻就执行(由于咱们不会知道用户什么时候执行操做,所以这个时间也被认为是异步的),跟 timeout 事件初始化同样,它也会放入到队列里面稍后执行。线程
在初始化Javascript代码块执行完后浏览器立刻会问一个问题:“还有谁?!谁在等着被执行?!” 在这个图的案例里面,鼠标点击回调事件和定时器的回调事件都在等着。浏览器会选择下一个队列事件(点击回调事件)并当即执行。timeout 的回调时间则会等待下一个时机去执行。翻译
注意,当鼠标点击事件回调在执行的时候,第一个 interval 事件回调到达时间点执行,它会跟 timeout 的回调事件同样进入队列等待稍后执行。可是请注意,当 interval 再次被触发(当 timeout 的回调事件还在执行)的时候,这个 interval 的回调执行会被放弃。假如在执行大块代码块的时候对全部的 interval 回调进行排队,那么这一系列的回调之间将不会有延迟。但相反,浏览器事实上每每只是等待,在更多的其余的事件入队以前不会再有其余的 interval 回调事件(指相同ID interval衍生出来的回调事件) 会被放入队列。【译者补充:就是说,当一个占用时间较长的事件在执行的时候,若是队列中已经有一个相同ID interval产生的且还没执行的回调事件在,即便到达了再下一个得interval触发时间也不会有新的interval回调事件入队。】code
事实上咱们能够看到案例中第三个 interval 回调事件被触发的时候, 前一个 interval 回调事件正在执行。这告诉了咱们一个事实:interval 回调事件不会在乎什么当前执行的内容,它们会不加区分地进入队伍,即便回调事件之间的时间会被浪费掉。
最后在第二个 interval 回调事件执行完后,咱们能够看到在javascipt引擎的队列里面已经没有东西能够执行了。这意味着浏览器如今会等待新的异步事件的发生。在 interval 回调事件再次触发的时候时间线已经到达50ms了,这一次,由于没有其余代码块在执行,因此这个 interval 回调事件会立刻执行。
来看一个例子去更好地区分 setTimeout
和 setInterval
:
setTimeout(function(){ /* Some long block of code... */ setTimeout(arguments.callee, 10); }, 10); setInterval(function(){ /* Some long block of code... */ }, 10);
乍看之下,这两个代码片断彷佛实现的功能是一致的,但仔细看其实不一样。值得注意的是, 在前一个回调执行以后,setTimeout 的代码至少会有10ms的延迟才会执行(可能会更多,但不会更少)。 setInterval 则在每10ms的时间里都会尝试去执行回调,而不会管前一个回调执行的完成时间。
今天学习了不少,咱们来回顾一下:
这些都是重要的基础知识。了解Javascript引擎的工做原理,特别是遇到大量异步事件的状况下,能够在构建高级应用程序代码的基础层面上作好准备。
for (var i=0; i<5; i++){ console.log(i) }
第一题就是基础的不能再基础了顺序输出0 1 2 3 4
for (var i=0; i<5; i++){ setTimeout(function(){ console.log(i) }, 1000 * i); }
第二题就有咱们上一节提到的知识了,javascript会先把for循环执行完,把setTimeout的回调事件都放到事件队列中,等初始块代码执行完后再去处理事件队列里的回调事件,而这个时候,for局部里面的变量 i 已经一早递加为 5 了(注意,for循环是先执行语句3再去判断能不能执行内部语句的,因此 i 已是 5 了)。所以结果是:
//延迟 5 5 5 5 5
for (var i=0; i<5; i++){ (function(i){ setTimeout(function(){ console.log(i) }, 1000 * i); })(i); }
第三题用了一个匿名函数和立刻执行的传参来包住了setTimeout,就是说setTimeout 的 i 这个时候用的是闭包里面的局部变量 i,由于匿名函数的 i 传参不是引用传值而是数值传值,因此匿名函数里的 i 不会根据外面for循环的变量 i 的改变而改变。所以结果是:
//延迟 0 1 2 3 4
for (var i=0; i<5; i++){ (function(){ setTimeout(function(){ console.log(i) }, 1000 * i); })(i); }
第四题考察的就是闭包的知识了,由于没有值传入,因此setTimeout读的仍是已经跑完for循环的 i。所以结果是:
//延迟 5 5 5 5 5
for (var i=0; i<5; i++){ setTimeout((function(i){ console.log(i) })(i), 1000 * i); }
第五题setTimeout里面的回调函数被当即匿名调用了,因此先会跟正常输出同样,不会延迟执行。同时由于匿名函数没有返回值,因此 setTimeout 的回调函数是 undefined。输出结果是:
//无延迟 0 1 2 3 4
for (var i=0; i<5; i++){ setTimeout((function(i){ return function(){ console.log(i); } })(i), 1000); }
根据第五题的变形另外加一题,这题跟第五题的区别就是匿名函数有返回值,就是 setTimeout的回调函数就再也不是undefined了,因此输出结果是:
//延迟 0 1 2 3 4
[0] How JavaScript Timers Work [1] 例题来源,小芋头君知乎live(如禁止发布请告知删除)