JS引擎执行过程

0x00.基础

        js是单线程语言。浏览器端JS是以单线程方式运行的,js与UI渲染占用一个主线程。当然,JS可以开启多线程(webwork)。

        js是异步执行的。js的快速解析速度得益于异步执行。js与UI渲染占用同一个主线程,这时如果js进行高负载的数据处理容易造成阻塞,造成浏览器卡顿。js提供了异步操作,像定时器(setTimeout、serInterval)、ajax请求、I/O回调等。通过事件循环实现

0x01.js的执行过程

    大致分为三个步骤:

        1.语法分析

        2.预编译阶段

        3.解释执行阶段

    浏览器按顺序加载由<script>分割的代码块,加载第一个代码块后按以上顺序执行,之后再按顺序加载下一个代码块。

    通过词法分析->语法分析->语法树->预编译->开始解释执行。更多详情

1.1语法分析

        js脚本加载完代码块后,首先进行语法分析阶段。

        在这一过程中主要分析语法等是否正确,如果错误抛出语法错误,停止这一代码块的执行,开始执行下一代码块;如果正确,进入下一阶段

1.2预编译阶段

    进入这一过程之前,先了解一下js运行环境,js运行环境有三种:

        1.全局环境(window)

        2.函数环境,每一个函数就是一个作用域

        3.eval

预编译之前:

  1. 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
  2. 第一个脚本文件加载;
  3. 脚本加载完毕后,分析语法是否合法;
  4. 开始预编译 查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,将函数名作为GO属性,值为函数体;

预编译的过程大致如下:

  1. 创建AO对象(Active Object),执行期上下文。
  2. 寻找函数的形参和变量声明,将变量和形参名作为AO对象的属性名,值设定为undefined.
  3. 将形参和实参相统一,即更改形参后的undefined为具体的形参值。
  4. 寻找函数中的函数声明,将函数名作为AO属性名,值为函数体。

下面以一个例子说明:

<script>
var a = 1;
console.log(a);
function test(a) {
  console.log(a);
  var a = 123;
  console.log(a);
  function a() {}
  console.log(a);
  var b = function() {}
  console.log(b);
  function d() {}
}
var c = function (){
console.log("I at C function");
}
console.log(c);
test(2);
</script>

        说明:        

//创建全局对象
GO {}
// 预编译,查找变量声明,这步相当于变量声明提前,将值赋予undefind。查找函数声明,值为函数体。
GO1 {
    a: undefined,
    c: undefined,
    test: function(a) {
        console.log(a);
        var a = 123;
        console.log(a);
        function a() {}
        console.log(a);
        var b = function() {}
        console.log(b);
        function d() {}
    }
}
// 按照顺序依次赋值,执行到test(2)
GO2 {
    a: 1,
    c: function (){
        console.log("I at C function");
    }
    test: function(a) {
        console.log(a);
        var a = 123;
        console.log(a);
        function a() {}
        console.log(a);
        var b = function() {}
        console.log(b);
        function d() {}
    }
}

此时在未执行test(2)结果为

// 开始预编译test(2)

1.//执行test(2)之前先 生成 其AO对象
AO {}

2.//寻找形参与变量声明
AO1 {
    a: undefined,
    b: undefined
}

3.//形参和实参相统一, 就是将test(2)里面2赋值给a
AO2 {
    a: 2,
    b:undefind
}

4.找函数声明
AO3 {
    a:function a() {},
    b:undefined
    d:function d() {}
}

//预编译阶段结束

开始执行test(2)


var a = 1;
console.log(a);
function test(a) {
  console.log(a);// 输出functiona(){}
  var a = 123;   //执行到这里重新对a赋,AO对象再一次更新
  console.log(a);// 输出123

  function a() {}//预编译环节已经进行了变量提升,故执行时不在看这行代码
  console.log(a);// 输出123
  var b = function() {}//这个是函数表达式不是函数声明,故不能提升,会对AO中的b重新赋值
  console.log(b);//输出function(){}
  function d() {}
}
var c = function (){
console.log("I at C function");
}
console.log(c);
test(2);

预编译小结

  • 预编译两个小规则

    1. 函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)
    2. 变量 声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)
  • 预编译前奏

    1. imply global 即任何变量,如果未经声明就赋值,则此变量就位全局变量所有。(全局域就是Window)
    2. 一切声明的全局变量,全是window的属性; var a = 12;等同于Window.a = 12;
  • 函数预编译发生在函数执行前一刻。

1.3执行阶段

        JS是单线程的,但不代表JS执行过程只有一个线程参与,一共有四个线程参与该过程,但是只有JS引擎线程执行脚本程序,其他只是协助,不参与代码执行解析。以下线程参与:

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

总结来讲,只有JS引擎会执行JS脚本程序,其他三个线程只负责将满足条件的处理函数推进js事件队列,等待JS引擎线程执行。JS引擎会把待执行的事件分为两种任务--->

1.3.1 两种任务

在node和ES6里面,JS任务分为两种:在最新的ECMAScript里面,微任务称为jobs,宏任务为task。

        宏任务(macro-task):宏任务又按执行顺序分为同步任务和异步任务,同步任务指的是在JS引擎主线程上按顺序执行任务,当前一个任务执行完,后一个任务开始执行,形成一个调用栈,异步任务指的是不进入主线程,满足触发条件后,相关的线程会把该异步任务推到任务队列,等待JS引擎主线程内任务执行完后,去任务队列里面取任务执行。例如console.log就为同步任务,setTimeout就为异步任务。

        微任务(micro-task):微任务类似于promise,nextick

1.3.2事件循环

事件循环由可以理解为三部分组成:

  • 主线程执行栈
  • 异步任务等待触发
  • 任务队列:以队列的数据结构对事件任务进行管理,先进先出,后进后出

执行过程如下:

  • 首先执行宏任务的同步任务,在主线程形成一个执行栈,如图粉色部分。
  • 在执行栈中有一些异步的API如setTimeout等推到相应管理线程(setTimeout->定时器触发线程)进行监控和控制。如图蓝色部分。
  • 当异步任务事件满足一定条件之后,会把他们推到任务队列里面,等待主线程读取执行。如图绿色部分。
  • 当主线程的同步任务执行完毕之后。检查是否存在微任务,有的话执行所有微任务
  • 当主线程的同步任务与微任务执行完之后,会去读取任务队列,将里面的任务推到执行栈中,按任务队列顺序执行。
  • 之后循环反复,知道任务全部执行完毕,这就是事件循环。

下面以一个例子为例:

console.log('1');
//记作 setTime 1
setTimeout(function () {
    console.log('2');
    // set4
    setTimeout(function() {
        console.log('3');
    });
    // pro2
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5')
    })
})

// 记作 pro1
new Promise(function (resolve) {
    console.log('6');
    resolve();
}).then(function () {
    console.log('7');
    // set3
    setTimeout(function() {
        console.log('8');
    });
})

// 记作 setInter2
setInterval(function () {
    console.log('9');
    // 记作 pro3
    new Promise(function (resolve) {
        console.log('10');
        resolve();
    }).then(function () {
        console.log('11');
    })
})
console.log('12')

答案你自己分析运行哦

拓展思考

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

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

  • setTimeout实现setInterval只能通过递归调用

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

  • setInterval则是每次都精确的隔一段时间就向任务队列推入一个事件,无论上一个setInterval事件是否已经执行,所以有可能存在setInterval的事件任务累积,导致setInterval的代码重复连续执行多次,影响页面性能。

综合以上的分析,使用setTimeout实现计时功能是比setInterval性能更好的。

参考:https://heyingye.github.io/2018/03/26/js%E5%BC%95%E6%93%8E%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%EF%BC%88%E4%BA%8C%EF%BC%89/

https://juejin.im/post/5b879a9f6fb9a01a0f24a5e1

发布了23 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/chjunjun/article/details/103180006