Event Loop 那些事儿

Event Loop 那些事儿

咱们一般说 JavaScript 是单线程的,其实是指在 JS 引擎中负责解释和执行 JS 代码的线程只有一个,通常成为主线程,在这种前提下,为了让用户的操做不存在阻塞感,前端 APP 的运行须要依赖于大量的异步过程,因此固然浏览器中还存在一些其余的线程,好比处理 http 请求的线程、处理 DOM 事件的线程、定时器线程、处理文件读写的 I/O 线程等等。前端

异步过程

一个异步过程一般是这样的:web

  1. 主线程发起一个异步请求,相应的工做线程接收请求并告知主线程已收到;
  2. 主线程能够继续执行后面的代码,同时工做线程执行异步任务;
  3. 工做线程完成工做后通知主线程,主线程收到通知后调用回调函数。

消息队列和事件循环

异步过程当中,工做线程在异步操做完成后须要通知主线程,那么这个通知机制是怎样实现的呢?答案是利用消息队列和事件循环。消息队列是一个先进先出的队列,里面存放着各类消息,咱们能够简单的理解为消息就是注册异步任务时添加的回调函数;事件循环是指主线程重复从消息队列中获取消息、执行回调的过程,之因此称为事件循环,就是由于它常常被用相似以下方式来实现:promise

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

消息队列是一个存储着待执行任务的队列,其中的任务严格按照时间前后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。消息队列每次仅执行一个任务,在该任务执行完毕以后,再执行下一个任务。执行栈则是一个相似于函数调用栈的运行容器,当执行栈为空时 JS 引擎便检查消息队列,若是不为空消息队列便将第一个任务压入执行栈中运行。浏览器

下面咱们来看下述代码来验证咱们的想法:异步

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

new Promise(function(resolve) {
    console.log(1)

    for (var i = 0; i < 10000; i++) {
        i == 9999 && resolve()
    }

    console.log(2)
}).then(function() {
    console.log(5)
});

console.log(3);

比较吊诡的事情出现了,为何结果是“1, 2, 3, 5, 4”,而不是“1, 2, 3, 4, 5”呢?!

按道理来讲,执行 setTimeout 时由于延迟为0,因此 console.log(4) 直接插入至消息队列;建立 Promise 实例时同步执行其函数体内的代码,先打印 1,再循环10000次后执行 resolve 将 then 中的回调函数 console.log(5) 插入至消息队列,而后打印 2;最后执行 console.log(3) 打印 3;在主线程执行完成后读取消息队列,依次打印4和5函数

上面的想法固然是比较天真的,实际上浏览器中仅有一个事件循环,而后消息队列是能够有多个的。

macro-queue: script (总体代码), setTimeout, setInterval, setImmediate, I/O, UI Rendering
micro-queue: process.nextTick, Promise, Object.observe, MutationObserveroop

而且 micro-queue 的任务优先级高于 macro-queue 的任务优先级,这两个任务队列执行顺序以下:取1个 macro-task 执行之,而后把全部 micro-task 顺序执行完,再取 macro-task 中的下一个任务,以此类推依次进行。线程

优先级:process.nextTick > promise.then > setTimeout > setImmediate

Tip:process.nextTick 永远大于 promise.then 缘由其实很简单:在 NodeJS 中,_tickCallback 在每一次执行完 TaskQueue 中的一个任务后被调用,而在这个_tickCallback 中实质上干了两件事:code

  1. 执行掉 nextTickQueue 中全部任务
  2. 第一步执行完后,执行 _runMicrotasks 函数(执行 micro-task 中的部分,即 promise.then 注册的回调)

总结:浏览器环境通常只能有一个事件循环(实际上有两类:browsing contexts 和 web workers),而一个事件循环能够多个任务队列,每一个任务都有一个任务源。相同任务源的任务,只能放到一个任务队列中;不一样任务源的任务,能够放到不一样任务队列中。举个栗子,客户端可能实现一个包含鼠标键盘事件的任务队列,还有其余的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它,这样就能保证流畅的交互性,并且别的任务也能执行到。server

至此,再返回去看以前的代码就不难分析出:代码执行开始把 setTimeout 的回调插入至 macro-queue 中,而打印完1后把 promise.then 的回调函数插入至 micro-queue 中,总体代码执行完后,按照消息队列的优先级,先执行 micro-task 即打印5,最后执行 macro-task 即打印4。

相关文章
相关标签/搜索