本文首发在alloyteam团队博客,连接地址http://www.alloyteam.com/2015/10/turning-to-javascript-series-from-settimeout-said-the-event-loop-model/javascript
做为一个从其余编程语言(C#/Java)转到Javascript的开发人员,在学习Javascript过程当中,setTimeout()运行原理是我遇到的一个不太好理解的部分,本文尝试结合其余编程语言的实现,从setTimeout说事件循环模型css
setTimeout()方法不是ecmascript规范定义的内容,而是属于BOM提供的功能。查看w3school对setTimeout()方法的定义,setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式。html
语法setTimeout(fn,millisec),其中fn表示要执行的代码,能够是一个包含javascript代码的字符串,也能够是一个函数。第二个参数millisec是以毫秒表示的时间,表示fn需推迟多长时间执行。前端
调用setTimeout()方法以后,该方法返回一个数字,这个数字是计划执行代码的惟一标识符,能够经过它来取消超时调用。java
起初我对 setTimeout()的使用比较简单,对其运行机理也没有深刻的理解,直到看到下面代码web
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
在我最初对setTimeout()的认识中,延时设置为500ms,因此输出应该为Time elapsed: 500 ms。由于在直观的理解中,Javascript执行引擎,在执行上述代码过程当中,应当是一个由上往下的顺序执行过程,setTimeout函数是先于while语句执行的。但是实际上,上述代码运行屡次后,输出至少是延迟了1000ms。编程
联想起以往学习Java的经验,上述Javascript的setTimeout()让我困惑。Java对setTimeout的实现有多种API实现,这里咱们以java.util.Timer包为例。使用Timer在Java中实现上述逻辑,运行屡次,输出都是Time elapsed: 501 ms。vim
import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { public static void main(String[] args) { // TODO Auto-generated method stub long start = System.currentTimeMillis(); Timer timer = new Timer(); timer.schedule(new MyTask(start), 500); while (System.currentTimeMillis() - start < 1000) {}; } } class MyTask extends TimerTask { private long t; public MyTask(long start) { // TODO Auto-generated constructor stub t=start; } @Override public void run() { // TODO Auto-generated method stub long end = System.currentTimeMillis(); System.out.println("Time elapsed:"+(end - this.t)+ "ms"); } }
这里深究setTimeout()为何出现这一差别以前,先说说java.util.Timer的实现原理。浏览器
上述代码几个关键要素为Timer、TimerTask类以及Timer类的schedule方法,经过阅读相关源码,能够了解其实现。数据结构
Timer:一个Task任务的调度类,和TimerTask任务同样,是供用户使用的API类,经过schedule方法安排Task的执行计划。该类经过TaskQueue任务队列和TimerThread类完成Task的调度。
TimerTask:实现Runnable接口,代表每个任务均为一个独立的线程,经过run()方法提供用户定制本身任务。
TimerThread:继承于Thread,是真正执行Task的类。
TaskQueue:存储Task任务的数据结构,内部由一个最小堆实现,堆的每一个成员为TimeTask,每一个任务依靠TimerTask的 nextExecutionTime属性值进行排序,nextExecutionTime最小的任务在队列的最前端,从而可以现实最先执行。
根据上述源码分析能够总结出如上如所示的流程图,实际上,这是一个多生产者、单一消费者的生产者--消费者模型。此种方式的不足之处为当某个任务执行时间较长,超过了TaskQueue中下一个任务开始执行的时间,会影响整个任务执行的实时性。为了提升实时性,能够采用多个消费者一块儿消费来提升处理效率,避免此类问题。
看过了Java.util.Timer对相似setTimeout()的实现方案,继续回到前文Javascript的setTimeout()方法中,再来看看以前的输出为何与预期不符。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
经过阅读代码不难看出,setTimeout()方法执行在while()循环以前,它声明了“但愿”在500ms以后执行一次匿名函数,这一声明,也即对匿名函数的注册,在setTimeout()方法执行后当即生效。代码最后一行的while循环会持续运行1000ms,经过setTimeout()方法注册的匿名函数输出的延迟时间老是大于1000ms,说明对这一匿名函数的实际调用被while()循环阻塞了,实际的调用在while()循环阻塞结束后才真正执行。
而在Java.util.Timer中,对于定时任务的解决方案是经过多线程手段实现的,任务对象存储在任务队列,由专门的调度线程,在新的子线程中完成任务的执行。经过schedule()方法注册一个异步任务时,调度线程在子线程当即开始工做,主线程不会阻塞任务的运行。
这就是Javascript与Java/C#之类语言的一大差别,即Javascript的单线程机制。在现有浏览器环境中,Javascript执行引擎是单线程的,主线程的语句和方法,会阻塞定时任务的运行,执行引擎只有在执行完主线程的语句后,定时任务才会实际执行,这期间的时间,可能大于注册任务时设置的延时时间。在这一点上,Javascript与Java/C#的机制很不一样。
在单线程的Javascript引擎中,setTimeout()是如何运行的呢,这里就要提到浏览器内核中的事件循环模型了。简单的讲,在Javascript执行引擎以外,有一个任务队列,当在代码中调用setTimeout()方法时,注册的延时方法会交由浏览器内核其余模块(以webkit为例,是webcore模块)处理,当延时方法到达触发条件,即到达设置的延时时间时,这一延时方法被添加至任务队列里。这一过程由浏览器内核其余模块处理,与执行引擎主线程独立,执行引擎在主线程方法执行完毕,到达空闲状态时,会从任务队列中顺序获取任务来执行,这一过程是一个不断循环的过程,称为事件循环模型。
参考一个演讲中的资料,上述事件循环模型能够用下图描述。
Javascript执行引擎的主线程运行的时候,产生堆(heap)和栈(stack)。程序中代码依次进入栈中等待执行,当调用setTimeout()方法时,即图中右侧WebAPIs方法时,浏览器内核相应模块开始延时方法的处理,当延时方法到达触发条件时,方法被添加到用于回调的任务队列,只要执行引擎栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些知足触发条件的回调函数。
以演讲中的示例进一步说明
以图中代码为例,执行引擎开始执行上述代码时,至关于先讲一个main()方法加入执行栈。继续往下开始console.log('Hi')时,log('Hi')方法入栈,console.log方法是一个webkit内核支持的普通方法,而不是前面图中WebAPIs涉及的方法,因此这里log('Hi')方法当即出栈被引擎执行。
console.log('Hi')语句执行完成后,log()方法出栈执行,输出了Hi。引擎继续往下,将setTimeout(callback,5000)添加到执行栈。setTimeout()方法属于事件循环模型中WebAPIs中的方法,引擎在将setTimeout()方法出栈执行时,将延时执行的函数交给了相应模块,即图右方的timer模块来处理。
执行引擎将setTimeout出栈执行时,将延时处理方法交由了webkit timer模块处理,而后当即继续往下处理后面代码,因而将log('SJS')加入执行栈,接下来log('SJS')出栈执行,输出SJS。而执行引擎在执行万console.log('SJS')后,程序处理完毕,main()方法也出栈。
这时在在setTimeout方法执行5秒后,timer模块检测到延时处理方法到达触发条件,因而将延时处理方法加入任务队列。而此时执行引擎的执行栈为空,因此引擎开始轮询检查任务队列是否有任务须要被执行,就检查到已经到达执行条件的延时方法,因而将延时方法加入执行栈。引擎发现延时方法调用了log()方法,因而又将log()方法入栈。而后对执行栈依次出栈执行,输出there,清空执行栈。
清空执行栈后,执行引擎会继续去轮询任务队列,检查是否还有任务可执行。
到这里已经能够完全理解下面代码的执行流程,执行引擎先将setTimeout()方法入栈被执行,执行时将延时方法交给内核相应模块处理。引擎继续处理后面代码,while语句将引擎阻塞了1秒,而在这过程当中,内核timer模块在0.5秒时已将延时方法添加到任务队列,在引擎执行栈清空后,引擎将延时方法入栈并处理,最终输出的时间超过预期设置的时间。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
前面事件循环模型图中提到的WebAPIs部分,提到了DOM事件,AJAX调用和setTimeout方法,图中简单的把它们总结为WebAPIs,并且他们一样都把回调函数添加到任务队列等待引擎执行。这是一个简化的描述,实际上浏览器内核对DOM事件、AJAX调用和setTimeout方法都有相应的模块来处理,webkit内核在Javasctipt执行引擎以外,有一个重要的模块是webcore模块,html的解析,css样式的计算等都由webcore实现。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现,这里仍是继续以setTimeout为例,看下timer模块的实现。
Timer类是webkit 内核的一个必需的基础组件,经过阅读源码能够全面理解其原理,本文对其简化,分析其执行流程。
经过setTimeout()方法注册的延时方法,被传递给webcore组件timer模块处理。timer中关键类为TheadTimers类,其包含两个重要成员,TimerHeap任务队列和SharedTimer方法调度类。延时方法被封装为timer对象,存储在TimerHeap中。和Java.util.Timer任务队列同样,TimerHeap一样采用最小堆的数据结构,以nextFireTime做为关键字排序。SharedTimer做为TimerHeap调度类,在timer对象到达触发条件时,经过浏览器平台相关的接口,将延时方法添加到事件循环模型中提到的任务队列中。
TimerHeap采用最小堆的数据结构,预期延时时间最小的任务最早被执行,同时,预期延时时间相同的两个任务,其执行顺序是按照注册的前后顺序执行。
var start = new Date; setTimeout(function(){ console.log('fn1'); }, 20); setTimeout(function(){ console.log('fn2'); }, 30); setTimeout(function(){ console.log('another fn2'); }, 30); setTimeout(function(){ console.log('fn3'); }, 10); console.log('start while'); while (new Date - start < 1000) {}; console.log('end while');
上述代码输出依次为
start while end while fn3 fn1 fn2 another fn2
1.《Javascript异步编程》
2.JavaScript 运行机制详解:再谈Event Loophttp://www.ruanyifeng.com/blog/2014/10/event-loop.html
3.Philip Roberts: Help, I'm stuck in an event-loop.https://vimeo.com/96425312
4.How JavaScript Timers Work.http://ejohn.org/blog/how-javascript-timers-work/
5.How WebKit’s event model works.http://brrian.tumblr.com/post/13951629341/how-webkits-event-model-works
6.Timer实现.http://blog.csdn.net/shunzi__1984/article/details/6193023