“JavaScript 引擎”通常被称作一种 虚拟机。准确讲JavaScript 虚拟机是一种"进程虚拟机",专门设计来解释和执行的 JavaScript 代码。“进程虚拟机”不具备虚拟机的全部功能,只能够运行一个程序或者进程。
概念
JavaScript 引擎的基本工作是把开发人员写的 JavaScript 代码转换成高效、优化的代码,这样就可以通过浏览器进行解释甚至嵌入到应用中。事实上,JavaScriptCore 自称为“优化虚拟机” 。
更准确地讲,每个 JavaScript 引擎都实现了一个版本的 ECMAScript,JavaScript 是它的一个分支。随着 ECMAScript 的不断发展,JavaScript 引擎也不断改进。之所以有这么多不同的引擎,是因为它们每个都被设计运行在不同的 web 浏览器、headless 浏览器、或者像 Node.js 那样的运行时环境中。
注意,浏览器中包括排布页面布局的布局引擎和解释和执行代码的底层JavaScript引擎。
学过编译原理的人都知道,对于静态语言来说(如Java、C++、C),处理上述这些事情的叫编译器(Compiler),相应地对于JavaScript这样的动态语言则叫解释器(Interpreter)。这两者的区别用一句话来概括就是:编译器是将源代码编译为另外一种代码(比如机器码,或者字节码),而解释器是直接解析并将代码运行结果输出。 比方说,firebug的console就是一个JavaScript的解释器。
但是,现在很难去界定说,JavaScript引擎它到底算是个解释器还是个编译器,因为,比如像V8引擎,它其实为了提高JS的运行性能,在运行之前会先将JS编译为本地的机器码(native machine code),然后再去执行机器码(这样速度就快很多),利用JIT。
FAQ
- 那什么是 headless 浏览器呢?它是一个没有图形用户界面的 web 浏览器。它们在对 web 产品进行自动化测试时十分有用。一个很棒的例子就是 PhantomJS。
- 那 Node.js 又和 JavaScript 引擎有什么关系?Node.js 是一个异步的、事件驱动的框架,让你在服务器端可以使用 JavaScript。既然他们是驱动 JavaScript 的工具,所以它们也是由 JavaScript 引擎驱动。
- JavaScript解析引擎与浏览器又是什么关系?JavaScript引擎是浏览器的组成部分之一。因为浏览器还要做很多别的事情,比如解析页面、渲染页面、Cookie管理、历史记录等等。那么,既然是组成部分,因此一般情况下JavaScript引擎都是浏览器开发商自行开发的。比如:IE9的Chakra、Firefox的TraceMonkey、Chrome的V8等等.
- JavaScript解析引擎与ECMAScript是什么关系?ECMAScript就是定义了语法规则。标准的JavaScript引擎就会根据这套文档去实现,注意这里强调了标准,因为也有不按照标准来实现的,比如IE的JS引擎。这也是为什么JavaScript会有兼容性的问题。
- 为什么JavaScript是单线程? 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
主流引擎
- Chrome: V8
- FireFox(Mozilla): JaegerMonkey(4.0以后)/SpiderMonkey
- IE: JScript
- Safari(WebKit): JavaScriptCore
目前,两个主要的引擎都利用了 NativeScript,它们分别是 WebKit 的 JavaScriptCore 和 Google 的 V8 引擎。这两个引擎使用不同的方式处理代码。
JavaScriptCore
- 它进行词法分析,就是将源代码分解成一系列具有明确含义的符号或字符串。
- 然后用语法分析器分析这些符号,将其构建成语法树。
- 接着四个 JIT(Just-In-Time)进程开始参与进来,分析和执行解析器所生成的字节码。
简单来说,JavaScript 引擎会加载你的源代码,把它分解成字符串(又叫做分词),再 把这些字符串转换 成编译器可以理解的字节码,然后执行这些字节码。
V8
Google 的 V8 引擎 是用 C++ 编写的,它也能够编译并执行 JavaScript 源代码、处理内存分配和垃圾回收。它被设计成由两个编译器组成,可以把源码直接编译成机器码:
- Full-codegen:输出未优化代码的快速编译器
- Crankshaft: 输出执行效率高、优化过的代码的慢速编译器
如果 Crankshaft 确定需要优化的代码是由 Full-codegen 生成的未优化代码,它就会取代 Full-codegen,这个过程叫做“crankshafting”。
一旦编译过程中产生了机器代码,引擎就会向浏览器暴露所有的数据类型、操作符、对象、在 ECMA 标准中指定的函数、或任何运行时需要使用的东西,NativeScript 就是如此。
任务队列
JavaScript语言的设计者意识到,主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种:
- 一种是同步(synchronous)任务
- 另一种是异步(asynchronous)任务
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
运行机制
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
事件和回调
事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
Event Loop
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
Event Loop上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function (){};
req.onerror = function (){};
req.send();
等价于:
var req = new XMLHttpRequest();
req.open('GET', url);
req.send();
req.onload = function (){};
req.onerror = function (){};
定时器
除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。
setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。
HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒。
需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
Node.js的Event Loop
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
Node.js Event Loop运行机制
- V8引擎解析JavaScript脚本。
- 解析后的代码,调用Node API。
- libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
- V8引擎再将结果返回给用户。
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:
- process.nextTick
- setImmediate
process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也即,它指定的任务总是发生在所有异步任务之前。
setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。
重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完(不支持递归),多个setImmediate可能则需要多次loop才能执行完。
网友评论