我以前写了一篇《怎么理解JavaScript异步机制的"诡异"》,初探了整个异步机制,但实际上真正在运行的时候,事件循环 EventLoop
并不仅是这么简单,这篇文章尝试深刻理解 JavaScript 事件循环。javascript
然而到底什么是事件循环?html
先跳出了 JavaScript 的范畴,先来看看更抽象的东西。java
要理解 事件循环(EventLoop)
首先先来理解,什么是事件驱动,我这里直接拿维基百科的解释:事件驱动程序设计。react
实际上 事件驱动(Event Driven)
普遍应用于GUI开发和异步IO开发,他也并非什么特别的东西,能够自行谷歌。c++
在事件驱动机制里面,主线程都会运行一个事件循环(EventLoop)
,有时候也叫 消息循环(MessageLoop)
或者 运行循环(RunLoop)
,顾名思义它就是一个循环,仍是一个无限循环。web
事件驱动的机制,一般还有事件或者消息队列,里面放着各类要处理的事件或者消息,经过这个循环不断的处理这些事件或者消息。面试
因此事件循环的背后就是事件驱动,而事件驱动是一种程序设计模型。shell
浏览器的话我选用了 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)
其余两种消息也是分别在不一样的 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,任务有五种:
V8
内部调用的MutationObserver
也是这一类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