链接

概述

JavaScript引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段。

  • 语法分析:分别对加载完成的代码块进行语法检验,语法正确则进入预编译阶段;不正确则停止代码块的执行,查找下一个代码块并进行加载,加载完成后再次进入该代码块的语法分析阶段。
  • 预编译:通过语法分析阶段后,进入预编译阶段,则创建变量对象(创建arguments对象(函数运行环境下),函数声明提前解析,变量提升),确定作用域链以及this指向。

思考

JavaScript是单线程的,为了避免代码解析阻塞使用了异步执行,异步执行机制是事件循环,理解事件循环就理解了JavaScript的异步机制。

JavaScript是单线程的,但并不代表参与JavaScript执行过程的线程就只有一个。会有四个线程参与改过程,但是只有JS引擎线程在执行JS脚本程序,其他的三个线程只是协助,不参与代码解析与执行。

参与js执行的线程是
  • JS引擎线程(JavaScript内核):负责解析执行JavaScript脚本程序的主线程(V8)
  • 事件触发线程:归属于浏览器内核进程,主要用于控制事件(鼠标,键盘),当该事件被触发的时候,事件触发线程就会把该事件的处理函数推进事件队列,等待JS引擎线程执行。
  • 定时器触发线程:主要控制计时器setInterval和定时器setTimeout,用于定时器的及时,计时完毕,满足定时器的触发条件,则将定时器的处理函数推进事件队列中,等待JS引擎线程执行。W3C标准规定setTimout低于4ms的时间间隔算为4ms。
  • HTTP异步请求线程:通过XMLHttpRequest连接后,通过浏览器新开的一个线程,监控readyState状态变更时,如果设置了该状态的回调函数,则将改状态的处理函数推进事件队列中,等待JS引擎的执行。浏览器对同一域名下的并发连接是由数量限制的chrome firefox6个 ,ie8 10个。

只有JS引擎在执行JS脚本程序,其他的三个线程只负责将满足触发条件的处理函数推进事件队列,等待JS引擎线程执行。

执行阶段

在es6或Node环境中。js的任务分为两种。宏任务(macro-task / task)和微任务(mocro-task / jobs)。

宏任务

宏任务分为同步任务异步任务

  • 同步任务指的是在js引擎主线程上按顺序执行的任务,任务依次执行,形成一个执行栈(函数调用栈)。
  • 异步任务指的是不进入JS主线程,而使满足触发条件时,相关的线程将该异步任务推进任务队列(task queue),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax,DOM事件。

事件循环(EventLoop)

事件循环由三部分组成:

  • 主线程执行栈
  • 异步任务等待触发
  • 任务队列
    JavaScript引擎的执行过程 - 图1

JS引擎主线程执行过程(事件循环的过程)

  • 执行宏任务的同步任务,在主线程上形成一个执行栈(函数调用栈)
  • 执行栈中的函数调用到一些异步执行的API(Ajax,DOM事件,setTimeout等API),会开启对应的线程(Http异步请求处理线程,事件触发线程和定时器触发线程)进行监控和控制。
  • 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(task queue)中等待主线程读取执行
  • 当jS引擎线程主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行。
  • 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环的过程

使用setTimeout模拟的setInterval和setInterval有什么区别吗?

  • setTimeout实现的setInterval只能通过递归调用。
  • setTimeout是到了指定的时间就把时间推到任务队列中,只有在任务队列中的setTimeout事件被主线程执行后,才会继续再次在到了指定时间的时候把事件推到任务队列,setTimout的事件执行肯定比指定的事件久。
  • setInterval则是每次都精确的间隔一段时间就向任务队列推入一个事件,无论上一个setInterval事件是否执行,所以有可能存在setInterval的事件任务积累,导致setInterval的代码重复连续执行多次,影响页面性能。

使用setTimeout模拟的功能比setInterval性能更好。如果不需要兼容低版本IE,最好使用requestAnimationFrame是更好的选择。

高频率触发事件,触发频率过高会影响页面的性能,造成页面卡顿,我们可以利用setTimout实现计时器的原理,对高频事件触发进行优化,实现点在于将多个触发事件合并成一个,这就是防抖节流

微任务

微任务是在es6和node环境中出现的一个任务类型,微任务(micro-task)的API主要有:Promise,process.nextTick
JavaScript引擎的执行过程 - 图2

  1. 执行宏任务中同步任务,执行结束;
  2. 检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件,推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务
  3. 执行新宏任务的事件任务,再检查是否存在可以执行的微任务,如此不断的重复循环。

例子

  1. console.log('script start');
  2. setTimeout(function() {
  3. console.log('setTimeout');
  4. }, 0);
  5. Promise.resolve().then(function() {
  6. console.log('promise1');
  7. }).then(function() {
  8. console.log('promise2');
  9. });
  10. console.log('script end');
  1. 代码块通过语法分析和预编译后,进入执行阶段,当JS引擎主线程执行到console.log(‘script start’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行;
  2. JS引擎主线程执行到setTimeout(function() { console.log(‘setTimeout’); }, 0);,JS引擎主线程认为setTimeout是异步任务API,则向浏览器内核进程申请开启定时器线程进行计时和控制该setTimeout任务。由于W3C在HTML标准中规定setTimeout低于4ms的时间间隔算为4ms,那么当计时到4ms时,定时器线程就把该回调处理函数推进任务队列中等待主线程执行,然后JS引擎主线程继续向下执行
  3. JS引擎主线程执行到Promise.resolve().then(function() { console.log(‘promise1’); }).then(function() { console.log(‘promise2’); });,JS引擎主线程认为Promise是一个微任务,这把该任务划分为微任务,等待执行
  4. JS引擎主线程执行到console.log(‘script end’);,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script end
  5. 主线程上的宏任务执行完毕,则开始检测是否存在可执行的微任务,检测到一个Promise微任务,那么立刻执行,输出promise1和promise2
  6. 微任务执行完毕,主线程开始读取任务队列中的事件任务setTimeout,推入主线程形成新宏任务,然后在主线程中执行,输出setTimeout

scriptmstart; script end; promise1; promise2, setTimout

JavaScript引擎的执行过程

原文

概述:

  1. JavaScript是单线程语言:在JavaScript默认一个页面只有一个线程在执行js脚本代码。
  2. JavaScript是单线程语言,代码解析迅速,不会发生解析阻塞。是异步执行,通过事件循环的方式来实现

一段代码:思考运行结果

  1. <script>
  2. console.log(fun)
  3. console.log(person)
  4. </script>
  5. <script>
  6. console.log(person)
  7. console.log(fun)
  8. var person = "Eric";
  9. console.log(person)
  10. function fun() {
  11. console.log(person)
  12. var person = "Tom";
  13. console.log(person)
  14. }
  15. fun()
  16. console.log(person)
  17. </script>

JavaScript的引擎执行过程分为三个阶段:1.语法分析 2.预编译阶段 3.执行阶段。

浏览器按照顺序加载<script></script>标签分割的代码块,加载js代码块完毕后,立刻进入上面三个阶段。然后再按照顺序查找下一个代码块,再继续执行以上三个阶段,无论是外部(没有设置异步加载)还是内嵌脚本,并且都在同一个全局作用域。

语法分析

主要作用是分析js代码块的语法是否正确,如果不正确就抛出错误。停止该代码块的执行,然后继续查找并加载下一个代码块,如果语法正确,则进入预编译阶段。

预编译阶段

<前置内容>Js的运行环境
运行环境主要分为三种

环境名称 描述
全局环境 JS代码加载完毕后,进入代码预编译即进入全局环境。
函数环境 函数调用执行时,进入该函数环境,不同的函数则函数环境不同。
eval 有安全、性能问题、不建议使用。

Js没进入一个不同的运行环境都会创建一个相应的执行上下文(Execution Context),那么再一段js程序中一般会创建多个执行上下文,js引擎以栈的方式对这些执行上下文进行处理,形成函数调用栈(call stack),栈底永远是全局执行上下文(Global Execution Context),栈顶则永远是当前执行上下文。

函数调用栈是使用栈存取的方式进行管理运行环境,先进后出、后进先出。

注意:不同的运行环境都会进入代码预编译和执行两个阶段,语法分析则是在代码块加载完毕时同意检验语法

创建执行上下文

执行上下文可以理解为当前的执行环境,与该运行环境相对应。创建执行上下文的过程中,主要做了三件事情

  1. 创建变量对象(Variable Object)
  2. 建立作用域链(Scope Chain)
  3. 确定this的指向

创建变量对象

创建变量对象主要经过以下几个过程

  1. 创建arguments对象,检查当前上下文中的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行,全局环境没有此过程。
  2. 检查当前上下文的函数声明,按照代码顺序,将找到的函数提前声明。如果当前的上下文对象没有还函数名属性,则再该变量对象以函数名建立一个属性,属性值为指向该函数所在堆内存地址的引用,如果存在,会被新的所覆盖。
  3. 检查当前上下文的变量声明,按照代码顺序查找,将找到的变量提前声明如果当前上下文的变量对象没有改变量的属性名,则在该变量对象以变量名建立一个属性,属性值为undefined,如果存在,则忽略声明。

在全局环境中,window对象就是全局执行上下文的变量对象,所有变量和函数都是window对象的属性和方法。所以函数声明提前和变量声明提前是在创建变量对象中进行的,且函数声明的优先级高于变量声明。

创建变量对象发生在预编译阶段,尚未进入执行阶段,该变量对象都是不呢个访问的,因此此时的变量属性尚未赋值。值仍为undefined,只有在进入执行阶段,变量中的变量属性进行赋值后,变量对象(Variable Object)转为活动对象(Active Object),才能进行访问,这个过程就是VO->AO的过程。

建立作用域链

作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

理清作用域链可以帮助我们解决许多的问题,比如说闭包。

作用域链的活动对象和变量可以直接理解为作用域。

  • 作用域链的第一项永远是当前的作用域(当前上下文的变量对象或活动对象)
  • 作用域链的最后一项永远是全局作用域(全局执行上下文的活动对象)
  • 作用域链保证了变量和函数的有序访问,查找到的方式是凶当前作用域逐级往上查找,找到则会停止查找。,找不到则一直查找到全局作用域,再找不到则会抛出引用错误

思考:闭包(Closure)是什么

闭包的定义可以总结为三点

  1. 在函数的内部定义新函数
  2. 新函数方位外层函数的局部变量,即访问外层函数环境的活动对象属性
  3. 新函数执行,创建新的函数执行上下文,外层函数即为闭包

确定this的指向

在全局环境下,全局上下文中变量对象的this属性指向window,函数环境下this指向较为灵活,需要根据环境和执行方法确定,需要举大量的典型例子概括

参考书籍,你不知道的JavaScript