参考链接:
(转载)js引擎的执行过程(一)

js引擎执行过程分为3个过程:

1、语法分析
2、预编译阶段
3、执行阶段
注:浏览器首先按顺序加载由script标签分割的js代码块,加载js代码块完毕后,立刻进入以上三个阶段,然后再按顺序查找下一个代码块,再继续执行以上三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。

1、语法分析:

js语法代码块加载完毕后,会首先进入语法分析阶段,主要作用是:

分析该js脚本代码块的语法是否正确,如果出现不正确,则向外抛出一个语法错误(SyntaxError),停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入预编译阶段

2、预编译阶段:

js代码块通过语法分析阶段后,语法正确则进入预编译阶段。在分析预编译阶段之前,我们先了解一下js的运行环境,运行环境主要有三种:

  • 全局环境(JS代码加载完毕后,进入代码预编译即进入全局环境)
  • 函数环境(函数调用执行时,进入该函数环境,不同的函数则函数环境不同)
  • eval(不建议使用,会有安全,性能等问题)

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

创建执行上下文:

可以理解为当前的执行环境,创建执行上下文的过程中,主要做了以下三件事:

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

    //首先创建fun执行上下文
    funEC = {
     //变量对象
     VO: {
         //arguments对象
         arguments: {
             a: undefined,
             b: undefined,
             length: 2
         },
    
         //test函数
         test: <test reference>, 
    
         //num变量
         num: undefined
     },
    
     //作用域链
     scopeChain:[],
    
     //this指向
     this: window
    }

    预编译阶段的变量对象都是在预编译阶段,没有进入执行阶段,变量对象都是不可以访问的,因为变量对象中的变量属性没有赋值,为undefined,只有到执行阶段,变量对象中的变量属性进行赋值,变量对象才能转为活动对象,才能进行访问,这个过程为VO->AO

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

var num = 30;

function test() {
    var a = 10;

    function innerTest() {
        var b = 20;

        return a + b
    }

    innerTest()
}
innerTestEC = {

    //变量对象
    VO: {b: undefined}, 

    //作用域链
    scopeChain: [VO(innerTest), AO(test), AO(global)],  

    //this指向
    this: window
}
test()

当调用到innerTest时,全局和test是执行阶段,innerTest是预编译,所以活动对象分别为AO,A0,VO,而innerTest的作用域链由当前执行环境的变量对象(没进入执行阶段前)与上层环境的一系列活动对象组成。

  1. 作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象)
  2. 最后一项永远是全局作用域
  3. 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。

3、确定this指向

在全局中国this指向window,函数环境的this指向却较为灵活,需要根据执行环境和执行方法确定。

3、执行阶段

js的异步执行机制是由事件循环event loop解决的。
js是单线程的,但是js执行过程会有4个线程,但是永远只有js引擎线程在执行js脚本程序,其他的三个线程只协助,不参与代码解析和执行。

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

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

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 内部(任务队列里)的任务,严格按照时间顺序压栈和执行的任务。
宏任务(macro-task)可分为同步任务和异步任务:

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

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

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

微任务通常来说就是需要在当前 task 执行结束后立即执行的任务,

问题:

1、js为什么是单线程的?

js最先设计被用在浏览器上,如果js是多线程的

场景描述:
那么现在有2个线程,process1 process2,由于是多线程的JS,所以他们对同一个dom,同时进行操作
process1 删除了该dom,而process2 编辑了该dom,同时下达2个矛盾的命令,浏览器究竟该如何执行呢?

2、js为什么需要异步?

如果JS中不存在异步,只能自上而下执行,如果上一行解析时间很长,那么下面的代码就会被阻塞。
对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验

3、js单线程是如果实现异步的?

通过的事件循环(event loop),理解了event loop机制,就理解了JS的执行机制

js中的event loop

事件循环可以理解成3部分组成,分别是:

  • 主线程执行栈
  • 异步任务等待触发
  • 任务队列
    任务队列就是以队列的数据结构对时间任务进行管理,特点是先进先出,后进后出

图片说明

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

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

微任务

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

  • 执行宏任务中同步任务,执行结束;

  • 检查是否存在可执行的微任务,有的话执行所有微任务,然后读取任务队列的任务事件,推进主线程形成新的宏任务;没有的话则读取任务队列的任务事件,推进主线程形成新的宏任务

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

console.log('script start');

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

//如果then之前有输出的话是当作同步处理的
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

代码块通过语法分析和预编译后,进入执行阶段,当JS引擎主线程执行到console.log('script start');,JS引擎主线程认为该任务是同步任务,所以立刻执行输出script start,然后继续向下执行;

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