我所理解的event loop

灵魂三问

  • JS为何是单线程的
    • 咱们都知道,JS是单线程的语言,那为何呢?个人理解是JS设计之初就是为了在浏览器端完成DOM操做和一些简单交互的,既然涉及到DOM操做若是是多线程就会带来复杂的同步问题,比较极端的例子就是两个线程可能一个在删除某个DOM节点一个却在修改这个DOM节点,这是浏览器以哪一个线程为准呢?
  • 为何须要异步
    • 若是没有异步,单线程的JS从上到下执行遇到一段代码须要执行比较久时就会阻塞页面的渲染,形成页面假死,这显然是一种不好的用户体验
  • 单线程又是如何实现异步的呢
    • 单线程之因此能实现异步是由于在处理异步任务时并非立刻运行的,而是经过一个事件循环(event loop)机制来管理任务的执行。貌似是浏览器提供了这么一个管理任务的event loop线程,它来管理和负责向咱们的主线程上输送须要执行的任务。因此理解了event loop的机制才能更好地理解JS的运行机制,理解JS的运行机制才能更准确地把握咱们代码的运行规律。

由一个面试题引起的思考

首先来看一道考察JS执行机制的面试题,原题是今日头条的前端面试题,我稍微进行了一点改造:html

async function async1() {
  console.log( 'async1 start' )
  await async2()
  console.log( 'async1 end' )
}

async function async2() {
  new Promise( function ( resolve ) {
    console.log( '11' )
    resolve();
  }).then( function () {
    console.log( '22' )
  })
}

console.log( 'script start' )

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

async1();

new Promise( function ( resolve ) {
  console.log( 'promise1' )
  resolve();
}).then( function () {
  console.log( 'promise2' )
})

console.log( 'script end' )

看完以后若是不在浏览器端运行一下你能有本身的答案吗,而且能自圆其说吗?获得答案都不难,放在浏览器里天然就有了输出:前端

script start
async1 start
11
promise1
script end
22
promise2
async1 end
setTimeout

由于题目是变幻无穷的,平常开发中的状况也是多种多样的,正确理解了其中的执行规律才能更好地开发,固然若是你获得的结果是同样的而且可以自圆其说那也就不必看下去了,若是你还对这个结果有点懵那就听听个人理解吧。node

微任务与宏任务

事实上,咱们的JS代码用同步和异步这两种划分方式来决定执行的前后顺序显然是不够的,从而有了另外一种划分方式,具体谁提出来的我就没考证了,大致上你们是这样分的:面试

  • macro-task(宏任务):总体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise的then回调,process.nextTick(Node)

由于咱们上面的面试题是前端面试题,因此咱们讨论的都是浏览器环境下的表现,node环境下的event loop貌似有些不同,这里就不讨论了。segmentfault

event loop的运行机制

既然前端谈到了JS是单线程的,同时只能处理一个任务,而咱们又将各类各样的任务分为了宏任务和微任务,那到底哪一种任务先执行了,这个运行逻辑就是event loop的判断逻辑。promise

先说说个人理解,再来印证上面代码的运行顺序:浏览器

  • 一段代码执行时先执行宏任务中的同步代码
  • 若是遇到像setTimeout这类宏任务就会把代码方式【宏任务队列】中
  • 若是遇到像Promise.then()这类微任务会放入【微任务队列】
  • 在本轮宏任务中的同步代码执行完以后就会依次执行本轮微任务队列中的代码,而后执行下一轮中的宏任务代码

说回上面的面试题,咱们模拟event loop来首先给他们归个队多线程

  • 首先遇到了console.log( 'script start' ),直接打印
  • 而后遇到了setTimeout这个宏任务,就被推到宏任务队列中
  • 而后是async1(),直接打印同步代码console.log( 'async1 start' )
  • 关于await async2(),其实是从右到左先执行了async2()里的代码,而后遇到await从而交出线程的控制权的,async2()执行以后先打印了11,console.log( '22' )被推入了微任务队列
  • 接着执行Promise中的同步代码console.log( 'promise1' ),而后将console.log( 'promise2' )推入微任务队列
  • 接着打印同步代码console.log( 'script end' ),到此,全部同步代码执行完毕
  • 接下来就要执行本轮代码中的微任务队列了,因此先打印22,再打印promise2
  • 至此,await等待的async2()执行完了,能够执行await下面的代码了,因此接下来打印async1 end
  • 最后,就是执行下一轮event loop中的宏任务setTimeout,最后打印setTimeout

单独说一下async和await

async本质上是加上了Generator函数而且内置了执行器的一个语法糖,而且async函数返回的是Promise对象。惟一须要注意的是await后面不管接的是同步代码仍是异步代码都要等他们执行完毕才能执行await结果以后的代码。而且通过验证,当遇到await语句时是从右到左先执行的await后面的代码,而后才交出线程的控制权直到await等待的结果运行完毕。异步

总结

整体上,算是能对上面那个题目的执行过程有了一个能自圆其说的解释,只是吧,为何规则是这样的呢?这些规则怎么证伪呢?这是我查资料的时候最纠结的问题。后来跟同事交流了以后吧,以为也不必纠结,毕竟最终的解释器是C写的,不懂规则能够去看源码啊,但是我看不懂啊,哈哈。。。因此,既然你们大部分人都是这么说,也能说得通,暂且先记着吧,至少,仍是可以解释平常中形形色色代码的运行规律的。async

看了几篇不一样观点的文章以后从新梳理一下event loop的顺序:

  • 先执行宏任务中的同步代码
  • 执行栈清空以后查询任务队列
  • 若是任务队列中有微任务,先执行
  • 执行完了微任务以后开始下一轮event loop,执行队列中宏任务的异步代码

参考文章中有几篇文章比我讲的生动一些,没看懂的能够参考一下,我主要是梳理一下本身的理解,有不一样想法的欢迎交流。

参考文章

相关文章
相关标签/搜索