1、javascript引擎,主要包含堆栈和调用栈
2、Web API由浏览器提供包括,DOM操作,事件监听,timeout的函数。
3、event loop(事件循环机制),提供javascript的执行机制。
一.变量的赋值
对于变量的赋值,有值赋值和引用赋值,值赋值var a =1
,引用赋值var obj = {a:1}
,由于对象是保存在堆中,只能通过引用去访问。
var obj_1={a:1};
var obj_2= obj_1//这里发生了对象的浅拷贝
变量赋值需要注意的一个关键点是:变量提升
console.log(a)//输出undefined;
var a = 3
为什么,因为当var a = 3
被引擎执行时候,是分三个步骤:
- 首先创建上下文,在堆栈中申请空间a,此时a表示的内存为存放任何值,顾表示为undefined。
- 上下文创建完成后,引擎开始执行,遇到
a=3
语句,就将a表示的内存值修改为3。
这种情况我们称为变量提升。在变量对象的创建过程中,函数声明的执行优先级会比变量声明的优先级更高一点,而且同名的函数会覆盖函数与变量,但是同名的变量并不会覆盖函数。
二.执行上下文与作用域(动态作用域 词法作用域)
1.执行上下文生命周期
当一个浏览器启动时候,会先构建一个全局执行上下文,当一个函数被调用时候,会构建一个函数执行上下文。执行上下文的生命周期分为三个步骤:
- 创建阶段:构建执行环境,比如生成VO(variable Object变量对象)、作用域链、this指向(this的值是动态绑定的)。上下文环境被压入调用栈。
- 执行阶段: 执行指令,对变量赋值,函数引用等。
- 执行结束: 执行上下文出栈,释放内存空间。
var global = 5;
funciton fn(g){
var k = 3;
return g+k;
}
var a = fn(global);
引擎分析这段代码并构建全局执行上下文
globalEC={//EC表示golbal execution
VO:{
global:undefined,
fn:函数的地址,
a:undefined
},
scopChain:[globalEC.Vo]//作用域链
this:undefined
}
构建完成后globalEC入栈,引擎根据上下文环境,开始执行代码:
执行->global=5;
修改 globalEC.VO.global:5
执行->function fn(){....}这是一个函数声明
执行-> a=fn(global);
这个时候构建fn的执行上下文环境:
fnEC={
VO:{
arguments: [5],
g: 5
k: undefined
},
scopChain: [fnEC.VO ,globalEC.VO]//作用域链
this: undefined
}
同上执行fn()中的代码,并修改,由于fn()是在全局上下文中被调用的,this指向全局对象(如果是在浏览器中fnEC.this= windows
)。
2. 要点
执行上下文的要点是,构建VO对象,和作用域链。作用域链保存了栈中执行上下文的VO,通过VO查询变量。这个机制使javascript具备了以下特性:
- 变量覆盖:通过作用域链查询变量时候,当遇到匹配的变量就不再往下查询。
- 闭包:当嵌套函数,被外部调用时候,他的作用域链,保持了对上层函数VO的引用,因此当上层函数执行完成并退栈的时候,并没有销毁VO的内存空间。
三. event loop机制
javascript是一个单线程语言,他的异步是通过事件循环机制模拟出来的。当浏览器启动的时候启动了如下进程:
- 一个 Browser 进程:浏览器界面显示交互;其他进程管理;网络管理
- 多个 Renderer进程:一个tab页一个进程;iframe由一个renderer进程管理;renderer是一个沙箱
- 一个GPU进程
Renderer进程启动了如下线程:
- GUI渲染线程:负责渲染浏览器界面,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
- JS引擎线程:负责处理Javascript脚本程序。(例如V8引擎)JS引擎线程负责解析Javascript脚本,运行代码。一个Renderer进程中只有一个JS引擎线程,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程:归属于浏览器而不是JS引擎,用来控制事件循环(点击,鼠标移动这些都是浏览器事件)事件触发之后会加入到事件队列等待执行
- 定时触发器线程:setInterval与setTimeout所在的线程。浏览器定时计数器并不是由JavaScript引擎计数的,是交给浏览器计时setTimeout(fn,ms) 指定某个任务在主线程最早可得的空闲时间执行,ms秒之后将fn函数加入到队列中W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。所以setTimeout的延时设置为0也不可能瞬发
- 异步http请求线程:在XMLHttpRequest连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
1. java事件循环
javascript是单线程的,单线程就是一个任务完成,才能执行下一个任务。如果一个任务调用了WebAPI例如setTimeout,ajax等,因为耗时,因此将事件回调放入task queue中,主线程则继续执行。于是任务分为两种:同步任务,异步任务。
- 同步任务由引擎的主线程执行,当前任务完成,才能执行下一个。
- 异步任务,不由引擎的主线程处,而是调用其他线程处理,并将线程事件的回调函数放入,事件队列中,等待引擎主线程调用。比如调用ajax,将由异步http请求线程处理网络数据的获取,如果获取数据成功,将成功事件的回调函数放入事件队列中,等待javascript引擎执行。
在事件队列可以分为task(macro-task)和micro-task,根据HTML规范,可以看到渲染一次页面有如下步骤:
- 执行事件队列中最后一个函数
- 执行微任务队列中的所有回调函数
- 渲染页面
为了更好理解任务执行的过程,我们先看一个简单的例子
console.log(1);
setTimeout(()=>{console.log(2)},3000);
console.log(3);
执行顺序:1,3,2
分析执行过程:
主线程执行,console.log(1)进栈,打印 1
主线程执行,setTimeout进栈,异步调用,seTimeout由定时触发器线程执行。3秒后将回调函数放入事件队列。
主线程执行,console.log(3),打印3
第一次主线程执行完成,开始事件循环。
事件队列有 回调()=>{console.log(2)}
, 主线程执行,打印2j
继续下一个例子:
console.log('main1');
setTimeout(function() {
console.log('setTimeout');
Promise.resolve().then(function() {
console.log('promise timeout');
});
}, 0);
new Promise(function(resolve, reject) {
console.log('promise');
resolve();
}).then(function() {
console.log('promise then');
});
console.log('main2');
执行顺序:main1,promise,main2,promise then,setTimeout
引擎主线程执行整个代码可以看做是一个宏任务因此输出:main1,promise,main2。这里需要注意,主线程执行的new Promise(function(r,r)){..})
这个构造函数的参数是一个函数表达式,会马上执行,因此会打印promise。
主线程执行完成后,事件循环开始,检查微任务队列,有new Promise.then
注册的函数,取出放入主线程中执行。此时微任务队列清空,取出setTimeout注册到宏任务队列中的回调,交给主线程执行,输出setTimeout,然后发现Promise,将then中的回调放入微任务队列。
主线程执行完一个宏任务后,发现微任务队列中有函数,取出调用,打印promise timeout。