浏览器的事件循环

355 阅读5分钟

为什么JS在浏览器中有事件循环机制?

由于js在浏览器中需要操作DOM,以及实现用户与浏览器的交互。所以我们需要将js设置为单线程的。如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,那么浏览器就会不知道操作哪个了。

既然我们知道了js是单线程的,也就是一个时间只能做一件事。那么就引发了下面一个问题,如果我们写了一个5秒的定时器,那么当定时器运行的时候,js还能否操作其他事件呢?如果不能操作,那用户在这5秒内是什么都点不了的!

为此,我们需要一个机制。来解决我们上面的问题。也就是我们常说的event loop(事件循环)

Event Loop

我们在这里引入两个概念

  1. 调用栈(call stack)
  2. 消息队列(Message Queue)

调用栈

每调用一个函数,解释器就可以把该函数添加进调用栈,解释器会为被添加进来的函数创建一个栈帧(并立即执行。如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。函数执行完成,对应的栈帧立即被销毁

消息队列

消息队列中存放的东西,可以理解为回调函数。简单来说就是,主线程在空闲的时候,就执行消息队列中的事件。也就是在调用栈情空的时候(全部任务执行完),消息队列中的事件会移到调用栈中去执行。

function func1(){
    cosole.log(2);
}
function func2(){
    console.log(1);
    func1();
    console.log(3);
}
func2();
//1
//2
//3

1.这里给出了只用到调用栈的例子,执行顺序如下

  1. func2()进栈

  2. console.log(1);进栈执行

  3. console.log(1);执行完毕,出栈

  4. func1()进栈

  5. console.log(2);进栈执行

  6. console.log(2);执行完毕,出栈

  7. func1()执行完毕,出栈

  8. console.log(3);进栈执行

  9. console.log(3);执行完毕,出栈

  10. func2()执行完毕,出栈

2.再让我们看看调用栈和消息队列共用的例子

function func1(){
    cosole.log(1);
}
function func2(){
    setTimeout(()=>{
		console.log(2)
	},0);
    func1();
    console.log(3);
}
func2();
//1
//3
//2
  1. func2()进栈
  2. setTimeout(()=>{ console.log(2) },0); 进栈执行, console.log(2)进入消息队列中,暂不执行
  3. setTimeout出栈
  4. func1()进栈
  5. console.log(1);进栈执行
  6. console.log(1);执行完毕,出栈
  7. func1()执行完毕,出栈
  8. console.log(3);进栈执行
  9. console.log(3);执行完毕,出栈
  10. func2()执行完毕,出栈
  11. 调用栈清空,执行消息队列中console.log(2)压入调用栈
  12. 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
  1. new Promise进栈

  2. console.log(4)进栈执行后出栈

  3. resolve(5)进栈执行后出栈

  4. new Promise出栈

  5. func2()进栈

  6. setTimeout(()=>{ console.log(2); });进栈执行,console.log(2);进入消息队列中等待执行。setTimeout出栈

  7. func1()进栈执行

  8. console.log(1);进栈执行后出栈

  9. func1()出栈

  10. console.log(3);进栈执行后出栈

  11. 第一个p.then进栈,console.log(resolved);进入微任务队列

  12. p.then出栈,第二个then进栈 ,console.log(6);进入微任务队列

  13. fun2()出栈,调用栈清空,开始将微任务队列任务压入调用栈中

  14. console.log(resolved)进栈执行后出栈

  15. console.log(6)进栈执行后出栈,调用栈清空,开始将消息队列任务压入调用栈中

  16. console.log(2);进栈执行后出栈

这里我们大概的就把调用栈、消息队列、微任务队列了解清楚了。那么我们常说的宏任务和微任务是什么呢?

宏任务、微任务

  1. 我们可以将宏任务理解为调用栈+消息队列中的任务
  2. 将微任务理解为微任务队列

结合上面的任务实例,我们就可以理解以下概念(搬运) JS引擎线程首先执行主代码块。每次调用栈执行的代码就是一个宏任务,包括消息队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取消息队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。

在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾

所谓Event Loop可以用下面这张图解释,这就是所谓的浏览器的事件循环了 在这里插入图片描述 最后附上B站关于event loop的动画制作视频 event loop ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)

( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)

( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)

( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́) ( ̀⌄ ́)