javascript引擎执行的过程的理解--执行阶段

1、概述javascript

同步更新sau交流学习社区(nodeJSBlog):javascript引擎执行的过程的理解--执行阶段html

js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段,上篇文章咱们介绍了语法分析和预编译阶段,那么咱们先作个简单归纳,以下:java

一、语法分析: 分别对加载完成的代码块进行语法检验,语法正确则进入预编译阶段;不正确则中止该代码块的执行,查找下一个代码块并进行加载,加载完成再次进入该代码块的语法分析阶段。node

二、预编译:经过语法分析阶段后,进入预编译阶段,则建立变量对象(建立arguments对象(函数运行环境下),函数声明提早解析,变量声明提高),肯定做用域链以及this指向。git

若是对语法分析和预编译,还有疑问:javascript引擎执行的过程的理解--语法分析和预编译阶段es6

 

 

本文主要分析js引擎执行的第三个阶段–执行阶段,在分析以前咱们先思考如下两个问题:github

一、js是单线程的,为了不代码解析阻塞使用了异步执行,那么它的异步执行机制是怎么样的?promise

答:经过事件循环(Event Loop),理解了事件循环的原理就理解了js的异步执行机制,本文主要介绍。浏览器

二、js是单线程的,那么是否表明参与js执行过程的线程就只有一个?数据结构

答:不是的,会有四个线程参与该过程,可是永远只有JS引擎线程在执行JS脚本程序,其余的三个线程只协助,不参与代码解析与执行。参与js执行过程的线程分别是:

(1)JS引擎线程: 也称为JS内核,负责解析执行Javascript脚本程序的主线程(例如V8引擎)。

(2)事件触发线程: 归属于浏览器内核进程,不受JS引擎线程控制。主要用于控制事件(例如鼠标,键盘等事件),当该事件被触发时候,事件触发线程就会把该事件的处理函数推动事件队列,等待JS引擎线程执行。

(3)定时器触发线程:主要控制计时器setInterval和延时器setTimeout,用于定时器的计时,计时完毕,知足定时器的触发条件,则将定时器的处理函数推动事件队列中,等待JS引擎线程执行。

注:W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms。

(4)HTTP异步请求线程:经过XMLHttpRequest链接后,经过浏览器新开的一个线程,监控readyState状态变动时,若是设置了该状态的回调函数,则将该状态的处理函数推动事件队列中,等待JS引擎线程执行。

注:浏览器对同一域名请求的并发链接数是有限制的,Chrome和Firefox限制数为6个,ie8则为10个。

 

总结:永远只有JS引擎线程在执行JS脚本程序,其余三个线程只负责将知足触发条件的处理函数推动事件队列,等待JS引擎线程执行。

 

2、执行阶段

先分析一个典型的例子(来自Tasks, microtasks, queues and schedules,建议英文基础好的阅读,很是不错的文章):

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

直接划分例子的代码结构,简单描述分析执行过程;

暂不解释该过程当中的概念和原理,概念和原理将会在下面具体讲解以下:

一、宏任务(macro-task)

宏任务(macro-task),宏任务又按执行顺序分为同步任务和异步任务

(1)同步任务

console.log('script start'); console.log('script end');

(2)异步任务

setTimeout(function() { console.log('setTimeout'); }, 0);

 

二、微任务(micro-task)

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

在JS引擎执行过程当中,进入执行阶段后,代码的执行顺序以下:

宏任务(同步任务) --> 微任务 --> 宏任务(异步任务)

输出结果:

script start
script end
promise1
promise2
setTimeout

 

进入ES6或Node环境中,JS的任务分为两种,分别是宏任务(macro-task)微任务(micro-task),在最新的ECMAScript中,微任务称为jobs,宏任务称为task,他们的执行顺序如上。可能不少人对上面的分析并不理解,那么咱们接下来继续对上面例子进行详细分析。

 

2.1宏任务

宏任务(macro-task)可分为同步任务异步任务

一、同步任务指的是在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,造成一个执行栈(函数调用栈)。

二、异步任务指的是不直接进入JS引擎主线程,而是知足触发条件时,相关的线程将该异步任务推动任务队列(task queue),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax,DOM事件,setTimeout等。

 

理解宏任务中同步任务和异步任务的执行顺序,那么就至关于理解了JS异步执行机制–事件循环(Event Loop)

 

2.1.1事件循环

事件循环能够理解成由三部分组成,分别是:

一、主线程执行栈

二、异步任务等待触发

三、任务队列

任务队列(task queue)就是以队列的数据结构对事件任务进行管理,特色是先进先出,后进后出

 

这里直接引用一张著名的图片(参考自Philip Roberts的演讲《Help, I’m stuck in an event-loop》),帮助咱们理解,以下:

在JS引擎主线程执行过程当中:

一、首先执行宏任务的同步任务,在主线程上造成一个执行栈,可理解为函数调用栈。

二、当执行栈中的函数调用到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。

三、当异步任务的事件知足触发条件时,对应的线程则会把该事件的处理函数推动任务队列(task queue)中,等待主线程读取执行。

四、当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推动主线程中,按任务队列顺序执行

五、当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(Event Loop)的过程。

 

若是仍是不能理解,那么咱们再次拿上面的例子进行详细分析,该例子中宏任务的代码部分是:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');

代码执行过程以下:

一、JS引擎主线程按代码顺序执行,当执行到console.log('script start');,JS引擎主线程认为该任务是同步任务,因此马上执行输出script start,而后继续向下执行。

二、JS引擎主线程执行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。因为W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推动任务队列中等待主线程执行,而后JS引擎主线程继续向下执行。

三、JS引擎主线程执行到console.log('script end');,JS引擎主线程认为该任务是同步任务,因此马上执行输出script end。

四、JS引擎主线程上的任务执行完毕(输出script start和script end)后,主线程空闲,则开始读取任务队列中的事件任务,将该任务队里的事件任务推动主线程中,按任务队列顺序执行,最终输出setTimeout,因此输出的结果顺序为script start script end setTimeout。     

以上即是JS引擎执行宏任务的整个过程。

 

理解该过程后,咱们作一些拓展性的思考:

咱们都知道setTimeout和setInterval是异步任务的定时器,须要添加到任务队列等待主线程执行,那么使用setTimeout模拟实现setInterval,会有区别吗?

答案是有区别的,咱们不妨思考一下:

一、setTimeout实现setInterval只能经过递归调用。

二、setTimeout是在到了指定时间的时候就把事件推到任务队列中,只有当在任务队列中的setTimeout事件被主线程执行后,才会继续再次在到了指定时间的时候把事件推到任务队列,那么setTimeout的事件执行确定比指定的时间要久,具体相差多少跟代码执行时间有关。

三、setInterval则是每次都精确的隔一段时间就向任务队列推入一个事件,不管上一个setInterval事件是否已经执行,因此有可能存在setInterval的事件任务累积,致使setInterval的代码重复连续执行屡次,影响页面性能。

综合以上的分析,使用setTimeout实现计时功能是比setInterval性能更好的。固然若是不须要兼容低版本的IE浏览器,使用requestAnimationFrame是更好的选择。

 

咱们继续再作进一步的思考,以下:

高频率触发的事件(例如滚动事件)触发频率太高会影响页面性能,甚至形成页面卡顿,咱们是否能够利用计时器的原理进行优化呢?

是能够的,咱们能够利用setTimeout实现计时器的原理,对高频触发的事件进行优化,实现点在于将多个触发事件合并成一个,这就是防抖节流,本文先不作具体讲解,你们能够自行研究,有机会我再另开文章分析。

 

2.2微任务

微任务是在es6和node环境中出现的一个任务类型,若是不考虑es6和node环境的话,咱们只须要理解宏任务事件循环的执行过程就已经足够了,可是到了es6和node环境,咱们就须要理解微任务的执行顺序了。微任务(micro-task)的API主要有:Promise, process.nextTick

这里咱们直接引用一张流程图帮助咱们理解,以下:

在宏任务中执行的任务有两种,分别是同步任务异步任务,由于异步任务会在知足触发条件时才会推动任务队列(task queue),而后等待主线程上的任务执行完毕,再读取任务队列中的任务事件,最后推动主线程执行,因此这里将异步任务即任务队列看做是新的宏任务。执行的过程如上图所示:

一、执行宏任务中同步任务,执行结束。

二、检查是否存在可执行的微任务,有的话执行全部微任务,而后读取任务队列的任务事件,推动主线程造成新的宏任务;没有的话则读取任务队列的任务事件,推动主线程造成新的宏任务。

三、执行新宏任务的事件任务,再检查是否存在可执行的微任务,如此不断的重复循环。

 

这就是加入微任务后的详细事件循环,若是尚未理解,那么们对一开始的例子作一个全面的分析,以下:

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

 

执行过程以下:

一、代码块经过语法分析和预编译后,进入执行阶段,当JS引擎主线程执行到console.log('script start');,JS引擎主线程认为该任务是同步任务,因此马上执行输出script start,而后继续向下执行。

二、JS引擎主线程执行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。因为W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推动任务队列中等待主线程执行,而后JS引擎主线程继续向下执行。

三、JS引擎主线程执行到Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });,JS引擎主线程认为Promise是一个微任务,这把该任务划分为微任务,等待执行。

四、JS引擎主线程执行到console.log('script end');,JS引擎主线程认为该任务是同步任务,因此马上执行输出script end。

五、主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个Promise微任务,那么马上执行,输出promise1promise2

六、微任务执行完毕,主线程开始读取任务队列中的事件任务setTimeout,推入主线程造成新宏任务,而后在主线程中执行,输出setTimeout

最后输出结果:

script start
script end
promise1
promise2
setTimeout

 

3、总结

以上即是JS引擎执行的所有过程,JS引擎的执行过程其实并不复杂,只要多思考多研究就能够理解,理解该过程后能够在必定程度上提升对JS的认识。

 

4、参考文献

相关文章
相关标签/搜索