JS引擎的执行机制
1.什么是JavaScript解析引擎
- js引擎就是能够读懂JavaScript代码,并准确地给出代码运行结果的一段程序。
- 对于静态语言(eg:Java、C++、C),处理上述事情的叫编译器,相应地对于JavaScript这样的动态语言则叫做解释器。区别:编译器是将源代码编译为另外一种代码(比如机器码或者字节码),而解释器是直接解析并将代码运行结果输出,比如firebug的console就是一个JavaScript解释器。
- V8引擎,为了提高JS的运行性能,在运行之前会将JS编译为本地的机器码,然后再去执行机器码。底层采用C/C++编写。
2.运行机制:
- 首先判断JS是同步还是异步,同步就进入主进程,异步就进入event table
- 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
- 同步任务进入主线程后一直进行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,有就推入主进程中。
macro-task(宏任务):包括整体代码script、setTimeout、setInterval
micro-task(微任务):Promise、process.nextTick
执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的“事件队列”中;
当前宏任务执行完成后,会查看微任务的“事件队列”,并将里面全部的微任务依次执行完。
setTimeout(function(){console.log("定时器开始啦")});
new Promise(function(resolve){
console.log("马上开始");
for(var i=0;i<1000;i++){i==99 && resolve();}
}).then(function(){console.log("执行then函数")});
console.log("代码执行完毕");
分析:
- 首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的“异步队列"Li
- 遇到new Promise直接执行,打印"马上执行for循环"
- 遇到then方法,是微任务,将其放到"微任务队列"中
- 打印"代码执行结束"
- 本轮宏任务执行完毕,查看本轮微任务,发现有一个then方法里的函数,打印"执行then函数"
- 待主进程中的同步任务执行完成之后,查看异步队列,执行console.log("定时器开始啦");
3.关于setTimeout
setTimeout(function(){console.log('执行了');});
3s后,setTimeout里的函数被推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。所以,只有同时满足(1)3s后(2)主线程空闲;才会3s后执行该函数。
4.关于JavaScript的单线程
为了利用多核CPU的计算能力,HTML5 提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个标准并没有改变JavaScript单线程的本质。
5.关于任务队列
- 由于单线程,所有任务需要排队,前一个任务耗时很长,后一个任务就不得不一直等着。
- 如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备很慢(比如Ajax操作从网络读取数据),不得不等这结果出来,再往下执行。
- 此时,主线程完全可以不管IO设备,挂起处于等待中的任务,线运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
- 于是:任务分为两种,同步任务和异步任务。同步任务:在主线程上排队执行的任务,著有前一个任务执行完毕,才能执行后一个任务;异步任务:不进入主线程,而是进入“任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
6.异步执行机制
- 所有同步任务都在主线程上执行,形成一个执行栈;
- 主线程之外,还存在一个“任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件;
- 一旦“执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
- 只要主线程空了,就会去读取“任务队列",这就是JavaScript的运行机制。
7.事件和回调函数
- “任务队列”是一个事件的队列,IO设备完成一项任务,就在“任务队列”中添加一个事件,表示相关的异步任务可以进入“执行栈”了。主线程读取“任务队列”,就是读取里面有哪些事件。
- “任务队列”中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(如鼠标点击、页面滚动等)。只要指定过回调函数,这些事件发生时就会进入“任务队列”,等待主线程读取。
- 所谓“回调函数”,就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
- “任务队列”是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列”上第一位的事件就自动进入主线程。但是,由于存在“定时器”功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
8.Node.js的EventLoop
- 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时执行。
参考:
《10分钟理解JS引擎的执行机制》 https://segmentfault.com/a/1190000012806637
《JavaScript运行机制详解:再谈Event Loop》 http://www.ruanyifeng.com/blog/2014/10/event-loop.html