JavaScript是一种解释型语言,也就是说,它不须要编译,能够由解释器实时运行。这样的好处是运行和修改都比较方便,刷新页面就能够从新解释;缺点是每次运行都要调用解释器,系统开销较大,运行速度慢于编译型语言。为了提升运行速度,目前的浏览器都将JavaScript进行必定程度的编译,生成相似字节码(bytecode)的中间代码,以提升运行速度。node
早期,浏览器内部对JavaScript的处理过程以下:web
读取代码,进行词法分析(Lexical analysis),将代码分解成词元(token)。
对词元进行语法分析(parsing),将代码整理成“语法树”(syntax tree)。
使用“翻译器”(translator),将代码转为字节码(bytecode)。
使用“字节码解释器”(bytecode interpreter),将字节码转为机器码。
逐行解释将字节码转为机器码,是很低效的。为了提升运行速度,现代浏览器改成采用“即时编译”(Just In Time compiler,缩写JIT),即字节码只在运行时编译,用到哪一行就编译哪一行,而且把编译结果缓存(inline cache)。一般,一个程序被常常用到的,只是其中一小部分代码,有了缓存的编译结果,整个程序的运行速度就会显著提高。ajax
不一样的浏览器有不一样的编译策略。有的浏览器只编译最常常用到的部分,好比循环的部分;有的浏览器索性省略了字节码的翻译步骤,直接编译成机器码,好比chrome浏览器的V8引擎。chrome
字节码不能直接运行,而是运行在一个虚拟机(Virtual Machine)之上,通常也把虚拟机称为JavaScript引擎。由于JavaScript运行时未必有字节码,因此JavaScript虚拟机并不彻底基于字节码,而是部分基于源码,即只要有可能,就经过JIT(just in time)编译器直接把源码编译成机器码运行,省略字节码步骤。这一点与其余采用虚拟机(好比Java)的语言不尽相同。这样作的目的,是为了尽量地优化代码、提升性能。下面是目前最多见的一些JavaScript虚拟机:跨域
JavaScript采用单线程模型,也就是说,全部的任务都在一个线程里运行。这意味着,一次只能运行一个任务,其余任务都必须在后面排队等待。浏览器
JavaScript之因此采用单线程,而不是多线程,跟历史有关系。JavaScript从诞生起就是单线程,缘由是不想让浏览器变得太复杂,由于多线程须要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来讲,这就太复杂了。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。缓存
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。网络
单线程模型带来了一些问题,主要是新的任务被加在队列的尾部,只有前面的全部任务运行结束,才会轮到它执行。若是有一个任务特别耗时,后面的任务都会停在那里等待,形成浏览器失去响应,又称“假死”。为了不“假死”,当某个操做在必定时间后仍没法结束,浏览器就会跳出提示框,询问用户是否要强行中止脚本运行。数据结构
若是排队是由于计算量大,CPU忙不过来,倒也算了,可是不少时候CPU是闲着的,由于IO设备(输入输出设备)很慢(好比Ajax操做从网络读取数据),不得不等着结果出来,再往下执行。JavaScript语言的设计者意识到,这时CPU彻底能够无论IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回告终果,再回过头,把挂起的任务继续执行下去。这种机制就是JavaScript内部采用的Event Loop。多线程
注:ajax同步异步可设置、用户/浏览器自执行事件(onclick、onload、onkeyup等等)以及定时器(setTimeout、setInterval)是异步操做。
所谓Event Loop,指的是一种内部循环,用来排列和处理事件,以及执行函数。Wikipedia的定义是:“Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)”
全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入“任务队列”(task queue)的任务,只有“任务队列”通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。
以Ajax操做为例,它能够看成同步任务处理,也能够看成异步任务处理,由开发者决定。若是是同步任务,主线程就等着Ajax操做返回结果,再往下执行;若是是异步任务,该任务直接进入“任务队列”,主线程跳过Ajax操做,直接往下执行,等到Ajax操做有告终果,主线程再执行对应的回调函数。
想要理解Event Loop,就要从程序的运行模式讲起。运行之后的程序叫作"进程"(process),通常状况下,一个进程一次只能执行一个任务。若是有不少任务须要执行,不外乎三种解决方法。
若是某个任务很耗时,好比涉及不少I/O(输入/输出)操做,那么线程的运行大概是下面的样子。
上图的绿色部分是程序的运行时间,红色部分是等待时间。能够看到,因为I/O操做很慢,因此这个线程的大部分运行时间都在空等I/O操做的返回结果。这种运行方式称为"同步模式"(synchronous I/O)。
若是采用多线程,同时运行多个任务,那极可能就是下面这样。
上图代表,多线程不只占用多倍的系统资源,也闲置多倍的资源,这显然不合理。
上图主线程的绿色部分,仍是表示运行时间,而橙色部分表示空闲时间。每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,而后接着日后运行,因此不存在红色的等待时间。等到I/O程序完成操做,Event Loop线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。
能够看到,因为多出了橙色的空闲时间,因此主线程得以运行更多的任务,这就提升了效率。这种运行方式称为"异步模式"(asynchronous I/O)。
这正是JavaScript语言的运行方式。单线程模型虽然对JavaScript构成了很大的限制,但也所以使它具有了其余语言不具有的优点。若是部署得好,JavaScript程序是不会出现堵塞的,这就是为何node.js平台能够用不多的资源,应付大流量访问的缘由。
若是有大量的异步任务(实际状况就是这样),它们会在“任务队列”中注册大量的事件。这些事件排成队列,等候进入主线程。本质上,“任务队列”就是一个事件“先进先出”的数据结构。好比,点击鼠标就产生一些列事件,mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。
之因此称为 事件循环,是由于它常常被用于相似以下的方式来实现:
while (queue.waitForMessage()) { queue.processNextMessage(); }
若是当前没有任何消息,queue.waitForMessage 会等待着同步将要到来的消息。
每个消息执行完成后,其它消息才会被执行。当你分析你的程序时,这点提供了一些优秀的特性,包括每当一个函数运行时,它就不能被抢占,而且在其余代码运行以前彻底运行(且能够修改此函数控制的数据)。这点与C语言不一样。例如,C语言中当一个程序在一个线程中运行时,它能够在任何点中止且能够在其它线程中运行其它代码。
这个模型的一个缺点在于当一个消息的完成耗时过长,网络应用没法处理用户的交互如点击或者滚动。浏览器用“程序须要过长时间运行”的对话框来缓解这个问题。一个比较好的解决方案是使消息处理变短且若是可能的话,将一个消息拆分红几个消息。
在浏览器里,当一个事件出现且有一个事件监听器被绑定时,消息会被随时添加。若是没有事件监听器,事件会丢失。因此点击一个附带点击事件处理函数的元素会添加一个消息。其它事件亦然。
调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段做为函数的第二个参数被传入。若是队列中没有其它消息,消息会被立刻处理。可是,若是有其它消息,setTimeout 消息必须等待其它消息处理完。所以第二个参数仅仅表示最少的时间 而非确切的时间。
零延迟 (Zero delay) 并非意味着回调会当即执行。在零延迟调用 setTimeout 时,其并非过了给定的时间间隔后就立刻执行回调函数。其等待的时间基于队列里正在等待的消息数量。在下面的例子中,"this is just a message" 将会在回调 (callback) 得到处理以前输出到控制台,这是由于延迟是要求运行时 (runtime) 处理请求所需的最小时间,但不是有所保证的时间。
(function () { console.log('this is the start'); setTimeout(function cb() { console.log('this is a msg from call back'); }); console.log('this is just a message'); setTimeout(function cb1() { console.log('this is a msg from call back1'); }, 0); console.log('this is the end'); })(); // "this is the start" // "this is just a message" // "this is the end" // "this is a msg from call back" // "this is a msg from call back1"
一个 web worker 或者一个跨域的 iframe 都有它们本身的栈,堆和消息队列。两个不一样的运行时只有经过 postMessage 方法进行通讯。这个方法会给另外一个运行时添加一个消息若是后者监听了 message 事件。