前言
Event Loop
是计算机系统的一种运行机制,是个很重要的概念。而Javascript
用这种机制来解决单线程运行带来的问题。理解很熟悉将会有利于我们更容易理解Vue
的异步事件。
JavaScript是单线程的
1、什么是单线程?
单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。简单来说,即同一时间只能做一件事件。
2、Js为什么是单线程?
Js
是一种运行在网页的简单的脚本语言,由于设计的初衷是作为浏览器脚本语言,用于与用户互动,以及操作DOM
。这决定它是单线程的。
3、单线程带来的问题?
单线程就意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就需要一直等着。这就会导致IO
操作(耗时但cpu闲置)时造成性能浪费的问题。
4、如何解决单线程的性能问题?
采用异步可以解决。主线程完全可以不管IO
操作,暂时挂起处于等待中的任务,先运行排在后面的任务。等到IO
操作返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。
执行栈
当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
];
- 全局上下文初始化
globalContext = {
VO: [global],
Scope: [globalContext.VO],
this: globalContext.VO
}
- 初始化的同时,
fun1
函数被创建,保存作用域链到函数的内部属性[[scope]]
fun1.[[scope]] = [
globalContext.VO
];
- 执行
fun1
函数,创建fun1
函数执行上下文,fun1
函数执行上下文被压入执行上下文栈
ECStack = [
fun1,
globalContext
];
-
fun1
函数执行上下文初始化:1.复制函数
[[scope]]
属性创建作用域链。2.用
arguments
创建活动对象。3.初始化活动对象,即加入形参、函数声明、变量声明。
4.将活动对象压入
fun1
作用域链顶端。
同时f
函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope: undefined,
f: reference to function f(){}
},
Scope: [AO, globalContext.VO],
this: undefined
}
- 执行
fun2()
函数,重复步骤2。 - 最终形成这样的执行栈:
ECStack = [
fun3
fun2,
fun1,
globalContext
];
-
fun3
执行完毕,从执行栈中弹出...一直到fun1
事件循环(Event Loop)
JavaScript内存模型
在了解事件循环之前,先要弄明白Js
的内存模型,这有助于更好的理解事件循环。
- 调用栈(Call Stack):用于主线程任务的执行。
- 堆(Heap):用于存放非结构数据,如程序分配的变量和对象。
- 任务队列(Queue): 用于存放异步任务。
Js异步执行的运行机制
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
任务
异步任务存放在任务队列里,异步任务分为 宏任务(macrotask)与微任务(microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。
宏任务主要包含:
-
script
(整体代码) setTimeout
setInterval
-
I/O
、UI
交互事件 -
setImmediate
(Node.js 环境)
微任务主要包含:
Promise
MutaionObserver
-
process.nextTick
(Node.js 环境)
我们的JavaScript
的执行过程是单线程的,所有的任务可以看做存放在两个队列中——执行队列和事件队列。
执行队列里面是所有同步代码的任务,事件队列里面是所有异步代码的宏任务,而我们的微任务,是处在两个队列之间。
当JavaScript
执行时,优先执行完所有同步代码,遇到对应的异步代码,就会根据其任务类型存到对应队列(宏任务放入事件队列,微任务放入执行队列之后,事件队列之前);当执行完同步代码之后,就会执行位于执行队列和事件队列之间的微任务,然后再执行事件队列中的宏任务。
实例
new Promise(resolve => {
resolve(1);
Promise.resolve().then(() => {
// t2
console.log(2)
});
console.log(4)
}).then(t => {
// t1
console.log(t)
});
console.log(3);
这段代码的流程大致如下:
-
script
任务先运行。首先遇到Promise
实例,构造函数首先执行,所以首先输出了 4。此时microtask
的任务有t2
和t1
-
script
任务继续运行,输出3
。至此,第一个宏任务执行完成。 - 执行所有的微任务,先后取出
t2
和t1
,分别输出2
和1
- 代码执行完毕
综上,上述代码的输出是:4321
事件循环
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop
(事件循环)。
从上图我们可以看出:
- 主线程运行的时候,产生堆(heap)和栈(stack)。
- 栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。
- 栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。
小结
事件循环其实并不难,多查阅资料,多看看相关例子就ok。希望一知半解的童鞋抓紧学习。