JavaScript 运行机制及Event Loop
堆
1.堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
a. 堆中某个节点的值总是不大于或不小于其父节点的值;
b. 堆总是一棵完全二叉树。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
2.堆是在程序运行时申请某个大小的内存空间,即动态分配内存;而栈只是指一种使用堆的方法。
栈
1.栈(stack)又名堆栈,是一个数据集合,也可以理解为只能在一端进行插入或删除操作的列表。这一端被称为栈顶,相对地,把另一端称为栈底。
2.栈就是一个桶,遵循先进后出原则;
3.栈(Stack)是操作系统在建立某个进程时或者线程(在支持多线程的操作系统中是线程)为这个线程建立的存储区域,该区域具有FIFO的特性,在编译的时候可以指定需要的Stack的大小。
队列
是一种支持先进先出(FIFO)的集合,即先被插入的数据,先被取出!
JavaScript运行机制:
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
为了利用多核CPU的计算能力,HTML5允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
执行栈
当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。 但是我们这里说的执行栈和上面这个栈的意义却有些不同。
js 在执行可执行的脚本时,首先会创建一个全局可执行上下文globalContext,每当执行到一个函数调用时都会创建一个可执行上下文(execution context)EC。当然可执行程序可能会存在很多函数调用,那么就会创建很多EC,所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。当函数调用完成,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境... 这个过程反复进行,直到执行栈中的代码全部执行完毕。
下面来看个简单的例子:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [
globalContext
];
2.全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
3.初始化的同时,fun1 函数被创建,保存作用域链到函数的内部属性[[scope]]
fun1.[[scope]] = [
globalContext.VO
];
4.执行 fun1 函数,创建 fun1 函数执行上下文,fun1 函数执行上下文被压入执行上下文栈
ECStack = [
fun1,
globalContext
];
5.fun1函数执行上下文初始化:
- 复制函数 [[scope]] 属性创建作用域链,
- 用 arguments 创建活动对象,
- 初始化活动对象,即加入形参、函数声明、变量声明,
- 将活动对象压入fun1 作用域链顶端。
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
6.执行 fun2() 函数,重复步骤2。
7.最终形成这样的执行栈:
ECStack = [
fun3
fun2,
fun1,
globalContext
];
8.fun3执行完毕,从执行栈中弹出...一直到fun1
事件队列
主线程执行栈执行时遇到异步任务,开辟分线程执行异步任务并在异步任务执行完后放入任务队列(注意,此时只是异步事件执行完成,其中的回调函数并没有去执行)。异步任务队列分为微任务队列和宏任务队列,同一次事件循环中,微任务永远在宏任务之前执行。主线程任务执行完会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出最早放入的事件放入执行栈中,执行其中的回调同步代码。如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”。
队列采取先进先出的原则;栈先进后出;
为了更好地理解Event Loop,请看下图
bg2014100802.png主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
macro task与micro task
在介绍之前,我们先看一段经典的代码执行:
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
会看到控制台先后分别输出:2、3、1。
先看一下阮老师对setTimeout的一些解释:
setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。
实际上,一般因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。
以下事件属于宏任务:
- setTimeout
- MessageChannel
- postMessage
- setImmediate
以下事件属于微任务
- new Promise()
- new MutaionObserver()
参考:
JavaScript 运行机制详解:再谈Event Loop
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
Event loop 机制简介