JS事件循环,了解一下?

2,552 阅读4分钟

在理解事件循环之前,我总会遇到一些奇奇怪怪的问题:比如明明已经调接口拿到了数据,可是跟在调数据之后的操作却没有正常执行;又或者不知道为啥,代码里非得加个setTimeout才能正常跑通;特别是在运用Promise的时候,更是有各种问题百思不得解。遇上问题要解决,更要知道问题产生的原因,这样才能hold住全场!

废话不多说了,先来看一段代码

console.log('start');

setTimeout(function(){
   console.log('setTImeout1') 
},0);

new Promise(function(resolve,reject){
    console.log('resolve')
    setTimeout(function(){
        console.log('setTimeout2')
    },200);
    resolve()
}).then(function(){
    console.log('then')
});

setTimeout(function(){
    console.log('setTimeout3')
},0);

console.log('end');

结果是start resolve end then setTimeout1 setTimeout3 settimeout2

在分析结果之前,我先来科普几个概念,这些概念的表述不一定与标准完全对应,但是可以帮助你更容易理解JS的事件机制

  • 宏任务(macro-task):包括js整体代码,setTimeout,setInterval,setImmediate ,I/O, UI renderder等
  • 微任务(micro-task):包括Promise,Object.observe,process.nextTick,MutationObserver等
  • 调用栈:js被加载进来之后,会从上至下读取代码,同步代码被立即执行,而异步代码被加入事件队列中
  • 事件队列:一些没有被立即执行的代码被添加到事件队列中,队列是一种先进先出的数据结构,也就是说,先加入事件队列的任务会被优先执行

我们知道,js是单线程的,这就是说,只有一个主线程,主线程会自上而下依次执行调用栈中的事件。任务队列中的代码被加载到函数调用栈中去执行。当前的宏任务队列中的代码执行完毕后,会执行本次宏任务队列中分发到微任务队列中的代码。然后执行下一个宏任务队列中的代码,依次循环。

这里要提一点容易误解的地方,setTimeout函数本身,其实是立即执行的,它内部的任务,才会被分发到任务队列中延时执行。


  • 代码被加载后,全局上下文进入函数调用栈,紧接着,‘start’被执行
  • 遇到setTimeout的时候,新建了一个宏任务队列,函数内的任务被分发这个队列中等待执行
  • 此时遇到了Promise,注意,Promise中的第一个function中的代码会立马开始执行,遇到resolve或者reject后,then方法中函数会被分发到本次事件循环的微任务队列中等待执行。所以这里立马打印出了'resolve'。遇到setTimeout2后,同样新建了一个宏任务队列,其中的函数被分发到了这个新的宏任务队列中,then方法中的操作被分发到了微任务队列中等待
  • 代码继续往下,遇到'setTimeout3'后再次新建了一个新的宏任务队列
  • 'end'被立即执行。此时有三个宏任务队列,一个微任务队列
  • 微任务队列中的操作被执行,也就是打印出了‘then’,此时,第一轮的事件循环结束。
  • 第一轮的事件循环结束,开始下一轮的事件循环,依次执行每个宏任务队列中的内容,我们这里宏任务队列中的函数比较简单,都是console操作,所以并没有再分发新的任务队列,但是由于第二个setTimeout设定了200毫秒的延时,所以‘setTimeout2’被最后打印。

说到这里,你基本上对事件循环有个大致的了解了。之前有个同学问过我一个问题,点击轮播图下一页,但是页面没有反应,代码是这样的:

这个函数是点击下一页的按钮后轮播图转动,他在getNextPhoto函数中调接口获取了下个页面的数据,goToPage函数里是让轮播图切换的操作。相信如果你读懂了这篇文章,就会知道问题出在了哪里。显然获取数据ajax是个异步操作,他被分发到了事件队列中等待执行,所以还没等数据回来,翻页的操作已经开始执行,由于没有数据,并没有按预期效果显示。解决方法就是在getNextPhoto函数里调接口拿到数据之后,再通知去执行goToPage操作,问题就解决了。

作为一个前端菜鸟,希望得到各位大神的批评指正!