前言
我之前写了一篇《怎么理解JavaScript异步机制的"诡异"》,初探了整个异步机制,但实际上真正在运行的时候,事件循环 EventLoop 并不只是这么简单,这篇文章尝试深入理解 JavaScript 事件循环。
然而到底什么是事件循环?
先跳出了 JavaScript 的范畴,先来看看更抽象的东西。
事件驱动
要理解 事件循环(EventLoop) 首先先来理解,什么是事件驱动,我这里直接拿维基百科的解释:事件驱动程序设计。
实际上 事件驱动(Event Driven) 广泛应用于GUI开发和异步IO开发,他也并不是什么特别的东西,可以自行谷歌。
在事件驱动机制里面,主线程都会运行一个事件循环(EventLoop),有时候也叫 消息循环(MessageLoop) 或者 运行循环(RunLoop),顾名思义它就是一个循环,还是一个无限循环。
事件驱动的机制,通常还有事件或者消息队列,里面放着各种要处理的事件或者消息,通过这个循环不断的处理这些事件或者消息。
所以事件循环的背后就是事件驱动,而事件驱动是一种程序设计模型。
Chromium 的消息循环
浏览器的话我选用了 Chromium 来举例,它对事件驱动的实现,其实叫 消息循环(MessageLoop)。
来看看 Chromium 处理自定义消息循环的源代码 message_pump_default.cc
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)
其他两种消息也是分别在不同的 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相关的消息
- 与平台无关的消息,例如
setTimeout的定时器就是属于这个 Chromium的 IO 操作是基于libevent实现,它本身也是一个事件驱动的库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
- 是个内存回收的清理任务,使用过 Java 的童鞋应该都很熟悉,只是在 JavaScript 这是
V8内部调用的 - 就是普通的回调,
MutationObserver也是这一类 - Callable
- 包括
Fullfiled和Rejected也就是 Promise 的完成和失败 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