聊一聊Event Loop

也许你听过Event Loop(事件循环),可能你会对它有点陌生,今天就来聊一聊 Event Loop 相关的东西。
提到 Event Loop,就必须从Javascript的单线程说起。

为什么Javascript 是单线程?

说到Javascript的单线程特点(也就是同一个时间,只能做一件事),你可能会有一点疑问,Javascript 为什么不能设计成多线程,在多线程操作下实现并行处理,效率不就更高吗?
Javascript的单线程特点,是跟它的用途有关的。众所周知,Javascript作为一种浏览器脚本语言,主要服务于用户与浏览器的交互,以及操作DOM。假设以下场景:javascript是多线程的,在同一时间,其中一个线程在操作某个DOM节点,并添加内容,而另一线程此刻却在执行清空该DOM内容,甚至是删除该DOM节点,浏览器就不知如何解析了。因此,为了避免复杂的同步问题,Javascript从诞生起,就决定了单线程的特性。

任务队列与同步、异步机制

问题来了,既然Javascript是单线程的,在同一时间只能执行特定的任务,并阻塞后续任务执行,这样Javascript的效率岂不是很低?Javascript设计者在最初的时候就考虑到了这一点。Javascript 的任务队列中的任务,主要有同步任务和异步任务组成。
同步任务,指主线程上排队执行的任务,只有当前一个同步任务执行完毕,后一个同步任务才能执行。
异步任务,指的是不进入主线程,而是进入到任务队列中,只有当任务队列,通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行机制:

1 所有同步任务在主线程执行,形成一个执行栈;
2 主线程之外,存在一个任务队列(task queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件;
3 一旦执行栈中所有同步任务执行完毕,系统读取任务队列。对应的异步任务,结束等待状态,进入执行栈,开始执行;
4 主线程重复上述步骤。

只要主线程空了,就会读取任务队列,这就是Javascipt的运行机制,整个过程不断重复。这种运行机制被称为Event Loop(事件循环)。

>

Event Loop

之所以称为 事件循环,是因为它经常被用于类似如下的方式来实现:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

如果当前没有任何消息,queue.waitForMessage 会等待着同步将要到来的消息。

Stack(栈)

Stack里存放着正在执行的任务。每个任务被称为帧,函数调用形成了一个栈帧。

1
2
3
4
5
6
7
8
9
10
function foo(b){
var a = 1;
return a + b + 3;
}
function bar(x){
var y = 2;
return foo(x * y);
}

console.log(bar(1));

上述代码在执行时,调用bar方法,在栈中创建了第一个帧,帧中包含 bar 的参数和局部变量。 当 bar 调用 foo 时,第二个帧被创建,帧中包含 foo 的参数和局部变量,并压到第一个帧之上。当 foo 返回时, 最上层的帧就被弹出栈(剩下bar 的调用帧)。当 bar 返回时, 栈就空了。

Heap(堆)

对象被分配到在一个堆中,一个用来表示内存中一大片非结构化区域。

来看一道题

1
2
3
4
5
6
7
8
9
10
11
12
13
(function test(){
setTimeout(function(){ console.log(1)}, 0);
new Promise(function(resolve){
console.log(2);
for(var i = 0; i < 1000; i++){
i == 999 && resolve();
}
console.log(3);
}).then(function(){
console.log(4);
})
console.log(5)
})()

上面代码执行输出为: 2,3,5,4,1,不知道是否与你预期的一样。

前面提到, javascript主线程,拥有一个执行栈以及一个任务队列。主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数执行完毕再将该函数出栈,直到所有代码执行完毕。
任务队列分为 macrotasks 和 microtasks, 每一次事件循环中, macrotask只会提取一个执行,而microtask会一直提取,直到microtask 队列清空。而事件循环每次只会入栈一个macrotask,主线程执行该任务后,又会先检查microtasks队列并完成里面的所有任务后再执行 macrotask。

macrotasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver

浏览器的事件循环模型:

1、选择最先进入 事件循环任务队列的一个任务, 如果队列中没有任务,则直接跳到第6步的 Microtask
2、设置 事件循环的当前运行任务为上一步所选择的任务
3、Run: 运行所选任务
4、设置 事件循环的当前运行任务为 null
5、将刚刚第3步运行的任务从它的任务队列中删除
6、Microtasks: perform a microtask checkpoint
7、更新并渲染界面
8、返回第1步

perform a microtask checkpoint 的执行步骤:

1、设置 performing a microtask checkpoint 的标记为 true
2、Microtask queue handling: 如果事件循环的 microtask queue 是空,跳到第8步 Done
3、选取最先进入 microtask queue 的 microtask
4、设置 事件循环的当前运行任务 为上一步所选择的任务
5、Run: 执行所选取的任务
6、设置 事件循环的当前运行任务 为 null
7、将刚刚第5步运行的 microtask 从它的 microtask queue 中删除
8、Done: For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object (此处建议查看原网页)
9、清理 Index Database 的事务
10、使 performing a microtask checkpoint 的标记为 false

再来看上面题目的执行过程:

1、当前task运行,执行代码,执行到setTimeout,将它的callback添加到 task queue;
2、实例化promise,输出 2; promise resolved;输出 3;
3、promise.then的callback被添加到microtasks queue中;
4、输出 5;
5、已到当前task的end,执行microtasks,输出 4;
6、执行下一个task,输出1。

参考