为什么要EventLoop?

  JS 作为浏览器脚本语言,为了避免复杂的同步问题(例如用户操作事件以及操作DOM),这就决定了被设计成单线程语言,而且也将会一直保持是单线程的。而在单线程中若是遇到了耗时的操作(IO,定时器,网络请求)将会一直等待,CPU利用率将会大打折扣,时间大量浪费。所以需要设计一种方案让一些耗时的操作放在一边等待,让后面的函数先执行,于是有了EventLoop的设计。

  将任务分为两种:

  • 同步任务
  • 异步任务
  1. 定时器都是异步操作
  2. 事件绑定都是异步操作
  3. AJAX中一般采取的异步操作(虽然也可以同步)
  4. 回调函数(不严谨的异步)

  任务都会按顺序进入调用栈(call stack),即图1-1的stack,然后按栈的顺序依次执行。若全是同步任务,就会正常地顺序执行。当遇到异步任务时(其实就是执行到了一个耗时的任务,它发起后,需要它的回调函数等待拿到结果之后才继续进行)将会放到WebAPIs中(图1-1),等待这个耗时操作返回结果,也有网友把这个 WebAPIs 称之为 Event Table。如果异步任务在WebAPIs中等待有了结果(比如setTimeout的时间截止了,xhr得到响应结果了,用户click事件发生了),就会将这个结果作为一个事件置于任务队列中。 【或者称之为:注册回调函数】

  那么任务队列又是什么?个人认为就是图中的callback queue,或称之为 Event Queue 。就是存放了各种耗时操作最后响应结果的各个事件(说白了,就是已经拿到结果的,就会从WebAPIs放到任务队列里来)

JS中EventLoop、宏任务与微任务的个人理解-LMLPHP图 1-1 转自Philip Roberts的演讲《Help, I'm stuck in an event-loop》

  搞懂上面两段话后,就可以谈EventLoop的作用了:

  • 在调用栈和任务队列之间进行“轮询”
  • 但轮询的规则是:只有每当调用栈为空,才能去“询问”任务队列中是否有事件需要处理
  • 若任务队列存在事件,则会将该事件相应的回调函数(异步操作)结束等待,置于调用栈中开始执行
  • 如果调用栈一直不为空,那就一直不会“询问”任务队列

  以上过程是不断循环的,js引擎中,存在一个叫monitoring process的进程,这个进程会不断的检查主线程的执行情况,一旦为空,就会去任务队列检查有哪些待执行的函数。这里的整个过程可以参考 一个工具 loupe 对整个调用过程进行查看。

  JS中EventLoop、宏任务与微任务的个人理解-LMLPHP

图 1-2 loupe, 也是从其他地方发现的这个东西,很直观

  针对call stack调用栈多说一句:通俗地讲,将调用栈比喻为程序员,各个任务比喻为需求,任务队列比喻为总监。当总监提需求时,程序员就要交接需求过来,然后完成它。如果没有需求,就一直等待总监给需求。给了就做,不给就等。

  搞懂同步任务与异步任务的具体执行流程后,再谈谈为什么要设计宏任务和微任务。

 为什么有宏任务、微任务?

  页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么若只有一种类型的任务就不合适了,所以引入了微任务队列。

  至此,任务队列已被分为:

  • 宏任务队列,即上文说的任务队列,callback queue,用于存放宏任务
  • 微任务队列,再开辟一个队列,用于存放微任务

JS中EventLoop、宏任务与微任务的个人理解-LMLPHP

图 2-1 微任务Microtask Queue的加入

  首先列举一下哪些是宏任务、哪些是微任务

  宏任务

  • script(主代码)
  • setTimeout()
  • setInterval()
  • postMessage
  • I/O
  • UI交互事件
  • setImmediate(Node.js)
  • requestAnimationFrame(浏览器)

  微任务

  • new Promise().then(回调)
  • MutationObserver(html5 新特性)
  • process.nextTick(Node.js)在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前

  紧接着第一节里说的EventLoop,当时没有考虑什么宏任务微任务,现在再加入微任务的概念再来考虑整个流程:

  1. 依旧是在调用栈和任务队列中轮询。(此时的任务队列指的是宏任务队列)
  2. 调用栈为空后,优先检查微任务队列,如果微任务队列中存在事件,则加入到调用栈中进行执行(为什么先询问的是微任务队列而不是宏任务队列,在后面解释)
    • 注:如果在执行微任务队列中的函数时,产生了新的微任务(比如then函数嵌套),则会继续在本次执行中执行(就是说如果期间一直有微任务产生,那就会永远卡在微任务队列执行)
  3. 如果微任务队列为空,那就取宏任务队列中的事件加入到调用栈中进行执行
  4. 若在执行宏任务的时候,产生了新的微任务,就会将该微任务加入到微任务队列,该微任务队列将会在下一次宏任务执行之前执行,如图2-2。
  5. 循环。

  依旧是:两个任务队列(宏、微)只有有任务,那么主进程的调用栈就会调过去执行,没有任务的话,主进程就一直等着,直到又有任务。

JS中EventLoop、宏任务与微任务的个人理解-LMLPHP

图 2-2 宏任务与微任务的执行顺序

  注意的是,图2-2看起来是宏任务先执行,微任务后执行,这仅仅是宏任务与微任务的先后次序,但不代表宏任务优先级比微任务高。事实是微任务的优先级是高于宏任务的。因为微任务其实是产生于宏任务的,不可能凭空产生微任务,也就不可能一开始就出现几个微任务。在本次宏任务产生微任务后,将会在下次宏任务执行之前,优先执行这些微任务。自然也就映证了设计微任务的初衷:为了让某些任务尽快执行。

  总结完整的EventLoop流程:

  1. 执行一个宏任务(调用栈中没有就从宏、微任务队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  5. 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

  微任务在本次宏任务之后执行,在本次渲染之前执行,在下次宏任务之前执行。(宏任务 -> 微任务 -> 渲染 -> 宏任务)

 包含宏任务、微任务的异步代码分析:

// 知乎作者:Miku
// 链接:https://zhuanlan.zhihu.com/p/257069622
// 注意:代码中的process.netxTick 函数存在于Node.js中
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5'); }); }); process.nextTick(function() { console.log('6'); }); new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8'); }); setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }); new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12'); }); });

 参考

04-01 04:31