深刻理解 JavaScript 事件循环,宏任务与微任务

前言

我以前写了一篇《怎么理解JavaScript异步机制的"诡异"》,初探了整个异步机制,但实际上真正在运行的时候,事件循环 EventLoop 并不仅是这么简单,这篇文章尝试深刻理解 JavaScript 事件循环。javascript

然而到底什么是事件循环?html

先跳出了 JavaScript 的范畴,先来看看更抽象的东西。java

事件驱动

要理解 事件循环(EventLoop) 首先先来理解,什么是事件驱动,我这里直接拿维基百科的解释:事件驱动程序设计react

实际上 事件驱动(Event Driven) 普遍应用于GUI开发和异步IO开发,他也并非什么特别的东西,能够自行谷歌。c++

在事件驱动机制里面,主线程都会运行一个事件循环(EventLoop),有时候也叫 消息循环(MessageLoop) 或者 运行循环(RunLoop),顾名思义它就是一个循环,仍是一个无限循环。web

事件驱动的机制,一般还有事件或者消息队列,里面放着各类要处理的事件或者消息,经过这个循环不断的处理这些事件或者消息。面试

因此事件循环的背后就是事件驱动,而事件驱动是一种程序设计模型。shell

Chromium 的消息循环

浏览器的话我选用了 Chromium 来举例,它对事件驱动的实现,其实叫 消息循环(MessageLoop)api

来看看 Chromium 处理自定义消息循环的源代码 message_pump_default.ccpromise

void MessagePumpDefault::Run(Delegate* delegate) {
  AutoReset<bool> auto_reset_keep_running(&keep_running_, true);

  for (;;) { // 这里是个死循环
#if defined(OS_MACOSX)
    mac::ScopedNSAutoreleasePool autorelease_pool;
#endif

    Delegate::NextWorkInfo next_work_info = delegate->DoSomeWork();
    bool has_more_immediate_work = next_work_info.is_immediate();
    if (!keep_running_)
      break;

    if (has_more_immediate_work)
      continue;

    has_more_immediate_work = delegate->DoIdleWork();
    if (!keep_running_)
      break;

    if (has_more_immediate_work)
      continue;

    if (next_work_info.delayed_run_time.is_max()) {
      event_.Wait();
    } else {
      event_.TimedWait(next_work_info.remaining_delay());
    }
    // Since event_ is auto-reset, we don't need to do anything special here
    // other than service each delegate method.
  }
}
复制代码

其余看不懂没关系,只要看出来它是个无限循环便可,Chromium 在这个无限循环里面,不断从队列里面取出消息并处理,有兴趣的童鞋能够点击下面的文章,扩展阅读:

理解WebKit和Chromium: 消息循环(Message Loop)

从Chrome源码看事件循环

其余两种消息也是分别在不一样的 MessageLoop 中处理。

能够看到事件循环自己的实现是经过它的宿主 Host 去实现的,因此在不一样的环境,例如 Node 甚至是不一样的浏览器,事件循环的机制实现都不同。

而我前面说到,事件驱动自己就是一种程序设计模型,实际上,浏览器上的事件循环,是有标准的,whatwg HTML5 有关 Event Loop Processing model 的章节就有详细的介绍事件循环的标准规范。

而 JavaScript 设计的巧妙之处是,JavaScript 的引擎,自己并不实现事件循环的机制,这也是为何 Node 可使用 V8 的缘由。

事件循环与宏任务,微任务的关系

在 HTML5 规范里面,有详细的介绍 宏任务 Macrotasks微任务 Microtasks 的实现规范。

实际上,之前是没有,宏任务和微任务的概念的,用回以前的这张图

这里面 Web APIs 的异步操做都会经历一次完整的事件循环,可是,有的时候,一些异步操做并不想要经历整个事件循环,所以微任务就出现了,能够查看下面的表分类。

任务 Chrome Node 分类
I/O Marco
requestAnimationFrame × Marco
setTimeout Marco
setInterval Marco
setImmediate × Marco
process.nextTick × Micro
MutationObserver × Micro
Promise Micro

在这一篇文章里面,有介绍道 微任务 Microtasks 的概念。

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.

对上面的进行解释的话,微任务能够用来调度,那些当且仅当执行脚本后立马执行的任务。例如:马上对一系列的动做作出回应,或者是不想承担一次完整异步队列代价的异步任务。

宏任务的本质

宏任务的本质就是:参与了事件循环的任务。

回到 Chromium 中,须要处理的消息主要分红了三类:

  • Chromium 自定义消息
  • Socket 或者文件等 IO 消息
  • UI 相关的消息
  1. 与平台无关的消息,例如 setTimeout 的定时器就是属于这个
  2. Chromium 的 IO 操做是基于 libevent 实现,它自己也是一个事件驱动的库
  3. UI 相关的其实属于 blink 渲染引擎过来的消息,例如各类 DOM 的事件

这些消息的具体任务又分红了不少不少种,具体能够参照源码 task_type.h

下面是其中一部分任务

......
  kJavascriptTimer = 10,
  kRemoteEvent = 11,
  kWebSocket = 12,
  kPostedMessage = 13,
  kUnshippedPortMessage = 14,
  kFileReading = 15,
  kDatabaseAccess = 16,
  kPresentation = 17,
  kSensor = 18,
  ....
复制代码

这些任务都是参与了完整的事件循环,其实与 JavaScript 的引擎无关,都是在 Chromium 实现的。

微任务的本质

微任务的本质:直接在 Javascript 引擎中的执行的,没有参与事件循环的任务。

微任务其实是真实的队列,具体的实现其实在 v8 的源码里面有 microtask.h,任务有五种:

  • FinalizationGroupCleanupJobTask
  • CallbackTask
  • CallableTask
  • PromiseReactionJobTask
  • PromiseResolveThenableJobTask
  1. 是个内存回收的清理任务,使用过 Java 的童鞋应该都很熟悉,只是在 JavaScript 这是V8内部调用的
  2. 就是普通的回调,MutationObserver 也是这一类
  3. Callable
  4. 包括 FullfiledRejected 也就是 Promise 的完成和失败
  5. Thenable 对象的处理任务

宏任务,微任务的优先级

前面说了宏任务和微任务的概念,用一个源代码来体现:

console.log('Start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('End');
复制代码

输出的顺序是:

Script Start
Script End
promise
setTimeout
复制代码

promise 是在当前脚本代码执行完后,马上执行的,它并无参与事件循环,因此它的优先级是高于 setTimeout

由于这段代码是在浏览器环境 Chrome 下面执行的,在不一样的地方,因为 EventLoop 实现不同,也会有不同的结果。

宏任务和微任务的总结

所以能够这么简单的总结:

  • 宏任务 Macrotasks 就是参与了事件循环的异步任务。
  • 微任务 Microtasks 就是没有参与事件循环的“异步”任务。

注意,微任务的“异步”,我是打了双引号的,根据我以前的证实,能够参照《怎么理解JavaScript异步机制的"诡异"》,宏任务的主要工做是在别的线程里面完成,完成后回调在主线程完成,但微任务实际上,它并无在别的线程上执行,它只是在当前 JavaScript 代码执行完后马上执行的,以此实现异步。

留下一个练习题

$(window).click(() => {
  console.log('clicked1');
  Promise.resolve().then(() => console.log('clicked promise1'));
  setTimeout(() => console.log('clicked timeout1'), 0);
});

$(window).click(() => {
  setTimeout(() => {
    console.log('clicked2');
    Promise.resolve().then(() => console.log('clicked promise2'));
    setTimeout(() => console.log('clicked timeout2'), 0);
  }, 0);
});

$(window).click(() => {
  Promise.resolve().then(() => {
    console.log('clicked3');
    Promise.resolve().then(() => console.log('clicked promise3'));
    setTimeout(() => console.log('clicked timeout3'), 0);
  });
});
复制代码

这个例子是我本身写的,而这三个事件的顺序变化,也会对最后结果的顺序有不一样的影响,能够加深你的了解。

了解事件循环以及宏任务,微任务的好处

除了面试之外,了解这些东西对实际开发真的会有好处吗?或者说花时间去了解这些东西真的划算吗?

答案是显然的。

了解 JavaScript 的底层运行机制,会让你有意识的在开发中避免掉一些坑,或者让你排查问题的时候,你更有方向性。

至少要理解,为何人们常说,不要阻塞事件循环 Don't break the EventLoop,得有清晰的认识。

另外强烈建议你观看,下面这个 youtube 的视频,看完之后你会对事件循环有了很是深入的体会。

What the heck is the event loop anyway? - Philip Roberts

参照文章

  1. 并发模型与事件循环

  2. 理解WebKit和Chromium: 消息循环(Message Loop)

  3. 从Chrome源码看事件循环

相关文章
相关标签/搜索