JS Runtime和引擎傻傻分不清
Runtime和引擎是两个容易混淆的概念。单单对于Javascript来说,他们是不一样的。
JS引擎的工作内容
JS引擎的工作内容通常包含JS代码的解析(对JS代码进行词法、语法等分析)和JIT编译(将JS代码编译为Intel, ARM以及MIPS等不同CPU对应的汇编代码),同时它还要负责执行代码、分配内存以及垃圾回收等功能。 流程如下图:
常见的JS引擎
| 引擎 | 描述 |
|---|---|
| Rhino | 由Mozilla基金会管理,开放源代码,完全以Java编写 |
| SpiderMonkey | 第一款JavaScript引擎,由BrendanEich在NetscapeCommunications时编写,用于Mozilla 1.0~3.0版本 |
| TraceMonkey | 基于实时编译的引擎,其中部份代码取自Tamarin引擎,用于 Mozilla Firefox 3.5~3.6版本 |
| JägerMonkey / JagerMonkey | 德文Jäger原意为猎人,结合追踪和组合码技术大幅提高效能,部分技术借鉴了V8、JavaScriptCore、WebKit,用于Mozilla Firefox 4.0以上版本 |
| V8 | 开放源代码,由Google丹麦开发,是Google Chrome的一部分 |
| JavaScriptCore | 用于Apple的Safari浏览器 |
| Chakra(JScript) | 用于微软IE系列 |
| Chakra(Javascript) | 用于微软Edge浏览器 |
| Hermes | 来自Facebook,生而与React Native紧密结合 |
| 其他 | 还有适用于物联网和嵌入式的轻量级引擎,如JerryScript、duktape、QuickJS等。 |
JS Runtime和JS引擎的关系
简单的说,JS引擎在JS Runtime中运行,JS runtime为引擎提供了一些内建的库,可以在程序运⾏时使⽤。
所以Window对象和DOM API,都存在于浏览器的Runtime而不存在于Node Runtime;相反,Cluster 和FileSystem API存在于Node Runtime确不存在与浏览器Runtime。当然,两个Runtime都包含内置的数据类型和常⽤的⼯具,⽐如Console对象,定时器等。
因此Chrome和Node.js共享相同的引擎(V8),但是它们具有不同的Runtime。
单线程和异步
单线程
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。 为什么JavaScript不能有多个线程呢?
JavaScript的单线程,与它的用途有关。 它是作为浏览器脚本语言而生的。其主要用途是与用户交互和操作DOM,这决定了它只能是单线程。 试想一下,如果JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器该怎么办? 为了避免这种问题,JavaScript语言的设计者最开始就采用了单线程的设计。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务,如此顺序执行。 JS的Runtime中,只有一个Call Stack(中文翻译为调用栈或者执行栈),用于执行JS代码,它遵循FILO(First-In-Last-Out)规则。 我们用下面动图来演示一个任务在Call Stack中的执行过程。
我们可以在浏览器中证明这一点,下图中f()方法抛出错误,我们可以在错误信息中依次看到调用关系,即:f被fn调用,fn被foo调用,foo被一个匿名函数调用。值得注意的是,最后的这个匿名函数在Call Stack中的位置是1,这是JS Runtime执行JS代码的主函数,也就是上图中的main()
因为Call Stack的空间有限,所以当你的代码中出现无限循环的情况时,就会爆出一个Ranger Error。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
异步
也是因为同一个时间只能运行一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。
再谈Eventloop
Eventloop
值得注意的是,JS Runtime却不是单线程的,这句话怎么理解呢? 以浏览器为例,当网络请求发出而未返回数据的时候,鼠标的点击事件还是可以被响应的,这归功于JS Rumtime的多线程。
在浏览器提供给JS Runtime,DOM操作、XHR、定时器等方法,我们称之为Web API。
异步方法调用了Web API,并将Callback(回调函数)挂起,待Web API返回结果之后,又会将Callback推入一个任务队列中,我们称为Callback Queue。 这个Callback Queue是FIFO(First-In-First-Out)的。当主线程的代码执行完成之后,就会去Callback Queue中轮询是否有Callback可以执行,如果有,就取出放入Call Stack中执行。
微任务和宏任务
异步任务又细分为Micro Task(微任务)和Macro Task(宏任务):
| 任务类型 | 举例 |
|---|---|
| Micro Task | process.nextTick、Promise和 MutationObserver 等。 |
| Macro Task | setTimeout、setInterval、setImmediate、 script中整体的代码、 I/O操作、 UI渲染等。 |
这里我们重点说一下process.nextTick和Promise。
process.nextTick方法可以在当前Call Stack的尾部, 下一次主线程读取Callback Queue之前触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。 Promise的回调函数会组成Promise Job Queue,在每个事件环的周期中,Runtime会先查看Promise Job Queue中有无任务。如果有,就先执行。如果没有再去执行Callback Queue中的任务。 流程如图:
下面给出一道题,大家可以练一练
process.nextTick(function(){
console.log(7);
});
setImmediate(function(){
console.log(9);
})
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
process.nextTick(function(){
console.log(8);
});
// 3 4 7 8 5 9
<本文完>
以上为笔者对JS引擎和事件环的一些理解,如有任何疑问或者勘误,欢迎在评论区留言~
谢谢~
参考文档:
www.ruanyifeng.com/blog/2014/1…
jakearchibald.com/2015/tasks-…
zhuanlan.zhihu.com/p/104333176