js中的事件循环和任务队列

523 阅读4分钟

一段Javascript代码是如何被调用的,执行过程又是什么样的?

首先我们都清楚,Javascript是单线程的,那么我们平时发送网络请求或者定时任务等,都是一些耗时的操作,这些任务执行的过程中,后面的代码一直阻塞吗?答案是否定的,那么浏览器是又是如何处理这种问题呢?

任务队列与循环事件

任务队列:顾名思义,就是放置等待执行的任务,形式如队列(先进先出原则); 任务队列分宏任务队列(Task Queue)和微任务队列(Microtask Queue),对应里面存放的就是宏任务和微任务。

  • 常见的宏任务:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js环境)
  • 常见的微任务:Promise.then、Object.observe、MutaionObserver、process.nextTick(Node环境)

事件循环:js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这一事件挂起,继续执行调用栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列中也就是任务队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕,当调用栈处于空闲状态时,会首先去查询任务队列是否有待执行的任务,如果有就拿出队列中的第一个任务,然后放到调用栈中去执行其中的同步代码,然后。。。如此反复,就形成了一个无限的循环,这个过程就是所谓的事件循环(Event Loop)。

上面我们提到了调用栈,那我们先解释一下调用栈为何为?

调用栈

Javascript运行的时候,主线程会形成一个栈,这个栈主要是解释器用来最终函数执行流的一种机制。通常这个栈被称为调用栈Call Stack,或者执行栈(Execution Content Stack).

调用栈,顾名思义是具有LIFO(先进后出,Last in First Out)的结构。调用栈内存放的是代码执行期间的所有执行上下文。

  • 每调用一个函数,解释器就会把该函数的执行上下文添加到调用栈并开始执行;
  • 正在调用栈中执行的函数,如果还调用了其他函数,那么新函数也会被添加到调用栈,并立即执行。
  • 当前函数执行完毕,解释器会将其执行上下文清除调用栈,继续执行剩余执行上下文中的剩余代码。

宏任务、微任务

这两个概念性的知识,我这里就不在赘述了,不熟悉的同学可以访问:juejin.cn/post/697987… 进行学习。

但是有一点需要特别注意:如果在执行微任务的过程中,产生新的微任务,同样会将该微任务添加到微任务队列中,V8引擎一直循环执行微任务队列中的任务,直到队列为空才算结束;也就是说在执行微任务过程中产生新的微任务并不会推迟到下个宏任务中执行,而是在当前宏任务中继续执行。

举例

async function async1(){
    console.log('1')
    await async2()
    console.log('2')
}
async function async2(){
    console.log('3')
}
console.log('4')
setTimeout(function(){
    console.log('5') 
},0)  
async1();
new Promise(function(resolve){
    console.log('6')
    resolve();
}).then(function(){
    console.log('7')
})
console.log('8')

那么结果是啥呢?

执行图.png

上面是一个大概的执行图,大概解释一下:

  • 1.加载整个js代码,然后开始执行main函数中的逻辑,首先打印4,setTimeout等待时间结束后,会把执行函数(function(){console.log(5)})放到宏任务队列;
  • 2.接下来开始执行 async1函数,然后打印1,接着遇到await关键字,那么它后面的代码会放到微任务队列中等待执行,执行async2,打印3;
  • 3.遇到 new Promise,那么就执行内部的函数,打印6,接着把then中的函数放到微任务队列中等待执行;
  • 4.打印主干中的8,此时调用栈已经为空,接下来开始依次执行微任务队列中的任务,打印2、7;
  • 5.微任务队列中任务队列清空后,开始依次执行宏任务队列中的任务,最后打印5,所以打印结果就是:4、1、3、6、8、2、7、5 你答对了吗?

await关键字:它只能用在aync定义的函数内,async函数会隐式地返回一个promise

所以顶上的async和await代码可以看作是这样的

function async2() {
  return new Promise((resolve, reject) => {
    console.log('3');
    resolve();
  });
}

async function async1() {
  console.log('1');
  await async2();
  console.log('2');
}
  • 这样的话,在async1中的await后面的console.log(2)就等价于在promise的then中执行,所以就会加入微任务队列中等待执行。

总结

这是我对调用栈事件循环微任务队列宏任务队列以及Promise、await的执行顺序的理解,如果有不同见解的,可以评论,如有问题会及时改正。