为什么JS在浏览器中有事件循环机制?
由于js在浏览器中需要操作DOM,以及实现用户与浏览器的交互。所以我们需要将js设置为单线程的。如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,那么浏览器就会不知道操作哪个了。
既然我们知道了js是单线程的,也就是一个时间只能做一件事。那么就引发了下面一个问题,如果我们写了一个5秒的定时器,那么当定时器运行的时候,js还能否操作其他事件呢?如果不能操作,那用户在这5秒内是什么都点不了的!
为此,我们需要一个机制。来解决我们上面的问题。也就是我们常说的event loop(事件循环)
Event Loop
我们在这里引入两个概念
- 调用栈(call stack)
- 消息队列(Message Queue)
调用栈
每调用一个函数,解释器就可以把该函数添加进调用栈,解释器会为被添加进来的函数创建一个栈帧(并立即执行。如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。函数执行完成,对应的栈帧立即被销毁
消息队列
消息队列中存放的东西,可以理解为回调函数。简单来说就是,主线程在空闲的时候,就执行消息队列中的事件。也就是在调用栈情空的时候(全部任务执行完),消息队列中的事件会移到调用栈中去执行。
function func1(){
cosole.log(2);
}
function func2(){
console.log(1);
func1();
console.log(3);
}
func2();
//1
//2
//3
复制代码
1.这里给出了只用到调用栈的例子,执行顺序如下
-
func2()进栈
-
console.log(1);进栈执行
-
console.log(1);执行完毕,出栈
-
func1()进栈
-
console.log(2);进栈执行
-
console.log(2);执行完毕,出栈
-
func1()执行完毕,出栈
-
console.log(3);进栈执行
-
console.log(3);执行完毕,出栈
-
func2()执行完毕,出栈
2.再让我们看看调用栈和消息队列共用的例子
function func1(){
cosole.log(1);
}
function func2(){
setTimeout(()=>{
console.log(2)
},0);
func1();
console.log(3);
}
func2();
//1
//3
//2
复制代码
- func2()进栈
- setTimeout(()=>{
console.log(2)
},0); 进栈执行,
console.log(2)进入消息队列中,暂不执行 - setTimeout出栈
- func1()进栈
- console.log(1);进栈执行
- console.log(1);执行完毕,出栈
- func1()执行完毕,出栈
- console.log(3);进栈执行
- console.log(3);执行完毕,出栈
- func2()执行完毕,出栈
- 调用栈清空,执行消息队列中console.log(2)压入调用栈
- console.log(2)执行完毕,出栈
这两个例子可以很好的解释调用栈和消息队列两个概念,接下来我们看一下另外一个概念——微任务队列(Microtask Queue)
微任务队列
微任务队列主要是执行Promise,process.nextTick任务。而执行顺序与上面的消息队列比较相似,同样是执行完调用栈中的任务,再调用。但是微任务队列会在消息队列之前被调用。调用顺序如下
调用栈 > 微任务队列 > 消息队列
3.再让我们看看调用栈、消息队列、微任务队列共用的例子(Event loop)
var p = new Promise(resolve =>{
console.log(4);
resolve(5)
})
function func1(){
console.log(1);
}
function func2(){
setTimeout(()=>{
console.log(2);
});
func1();
console.log(3);
p.then(resolved => {
console.log(resolved);
})
.then(()=>{
console.log(6);
})
}
func2()
//4
//1
//3
//5
//6
//2
复制代码
-
new Promise进栈
-
console.log(4)进栈执行后出栈
-
resolve(5)进栈执行后出栈
-
new Promise出栈
-
func2()进栈
-
setTimeout(()=>{
console.log(2);
});进栈执行,console.log(2);进入消息队列中等待执行。setTimeout出栈 -
func1()进栈执行
-
console.log(1);进栈执行后出栈
-
func1()出栈
-
console.log(3);进栈执行后出栈
-
第一个p.then进栈,console.log(resolved);进入微任务队列
-
p.then出栈,第二个then进栈 ,console.log(6);进入微任务队列
-
fun2()出栈,调用栈清空,开始将微任务队列任务压入调用栈中
-
console.log(resolved)进栈执行后出栈
-
console.log(6)进栈执行后出栈,调用栈清空,开始将消息队列任务压入调用栈中
-
console.log(2);进栈执行后出栈
这里我们大概的就把调用栈、消息队列、微任务队列了解清楚了。那么我们常说的宏任务和微任务是什么呢?
宏任务、微任务
- 我们可以将宏任务理解为调用栈+消息队列中的任务
- 将微任务理解为微任务队列
结合上面的任务实例,我们就可以理解以下概念(搬运)
JS引擎线程首先执行主代码块。每次调用栈执行的代码就是一个宏任务,包括消息队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取消息队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。
在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾
所谓Event Loop可以用下面这张图解释,这就是所谓的浏览器的事件循环了
最后附上B站关于event loop的动画制作视频
event loop
( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)
( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)
( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)
( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)