基础
js是单线程语言。浏览器端JS是以单线程方式运行的,依赖于类型谷歌的v8引擎等各种js引擎,js又是解释型语言,不需要像Java那样先编译后执行。,js与UI渲染占用一个主线程。当然,通过webwork可以开启多线程。
js是异步执行的。js的快速解析速度得益于异步执行。js与UI渲染占用同一个主线程,这时如果js进行高负载的数据处理容易造成阻塞,造成浏览器卡顿。js提供了异步操作,像定时器(setTimeout、serInterval)、ajax请求、I/O回调等。通过事件循环实现
js的执行过程
大致分为三个步骤:
1.语法分析
2.预编译阶段
3.解释执行阶段
浏览器按顺序加载由<script>分割的代码块,加载第一个代码块后按以上顺序执行,之后再按顺序加载下一个代码块。
通过词法分析->语法分析->语法树->预编译->开始解释执行。更多详情
1语法分析
js脚本加载完代码块后,首先进行语法分析阶段。
在这一过程中主要分析语法等是否正确,如果错误抛出语法错误,停止这一代码块的执行,开始执行下一代码块;如果正确,进入下一阶段
2预编译阶段
进入这一过程之前,先了解一下js运行环境,js运行环境有三种:
1.全局环境(window)
2.函数环境,每一个函数就是一个作用域
3.eval
编译时每碰到一个运行环境就会创建一个执行上下文,推到栈内,形成函数执行栈。也就是说,每个函数有自己的执行环境。根据作用域链的规则,会优先在自己的作用域链找变量,然后去外层环境,因为js存在变量提升,所以会发生异常的输出。
因为预编译就发生在执行前,所以在创建执行上下文的时候会确定this指向。this指向的确立取决于当前的执行环境。
因为js的这一预编译过程,涉及到js的一些特性,比如执行上下文的问题,变量的声明提前,闭包,this指向的问题,静态作用域链是什么等。https://github.com/mqyqingfeng/Blog 这里对这些概念解释的比较清楚
预编译之前:
- 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
- 第一个脚本文件加载;
- 脚本加载完毕后,分析语法是否合法;
- 开始预编译 查找变量声明,作为GO属性,值赋予undefined; 查找函数声明,将函数名作为GO属性,值为函数体;
预编译的过程大致如下:
- 创建AO对象(Active Object),执行期上下文。
- 寻找函数的形参和变量声明,将变量和形参名作为AO对象的属性名,值设定为undefined.
- 将形参和实参相统一,即更改形参后的undefined为具体的形参值。
- 寻找函数中的函数声明,将函数名作为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);
预编译小结
- 预编译两个小规则
- 函数声明整体提升-(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)
- 变量 声明提升-(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)
- 预编译前奏
- imply global 即任何变量,如果未经声明就赋值,则此变量就位全局变量所有。(全局域就是Window)
- 一切声明的全局变量,全是window的属性; var a = 12;等同于Window.a = 12;
- 函数预编译发生在函数执行前一刻。
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引擎会把待执行的事件分为两种任务--->
3.1 两种任务
在node和ES6里面,JS任务分为两种:在最新的ECMAScript里面,微任务称为jobs,宏任务为task。
宏任务(macro-task):宏任务又按执行顺序分为同步任务和异步任务,同步任务指的是在JS引擎主线程上按顺序执行任务,当前一个任务执行完,后一个任务开始执行,形成一个调用栈,异步任务指的是不进入主线程,满足触发条件后,相关的线程会把该异步任务推到任务队列,等待JS引擎主线程内任务执行完后,去任务队列里面取任务执行。例如console.log就为同步任务,setTimeout就为异步任务。
微任务(micro-task):微任务类似于promise,nextick
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性能更好的。