关于浏览器Event Loop

最近看到Event Loop这个词出现的频率有点高,于是查阅各方资料在此记录一下。

先不说概念,我们来看段代码:

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

复制这段代码到控制台,在Chrome会输出如下结果

关于浏览器Event Loop

Why?

如果想弄清楚原因,就必须得弄清楚今天要提到的概念Event Loop。

运行时概念

关于浏览器Event Loop

函数调用形成了一个栈帧。

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

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

对象被分配在一个堆中,即用以表示一大块非结构化的内存区域。

队列

一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。

在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

为什么JavaScript是单线程

稍理解JavaScript的都知道JavaScript是单线程,即同一时间只能处理一件事情。JavaScript为什么不能是多线程呢,这样就可以同时处理多件事情提高效率。
JavaScript的宿主最开始本身就是浏览器,处理用户的交互事件。作为浏览器脚本,它只能一次做一件事情,假如用户点击一个按钮的时候,需要删除一个节点,而另一段代码此时又要添加这个节点,那JavaScript该如何处理,以谁为准?
所以JavaScript在创造之初就考虑到了这点,也决定了它只能是单线程,这是它的核心特征之一。

Event Loop

既然JavaScript是单线程的,那就意味着任务需要排队,只有前一个任务执行完毕,下一个任务才能开始,于是就有了任务队列。如果一个任务耗时很长,下面的任务就得一直等着,明显不太合理,那么能否先把耗时很久的任务先挂起来,先执行后面的任务,等IO设备返回的结果,再去执行之前挂着的任务。

于是任务就可以分两种:同步任务和异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

异步任务指的是异步的代码加入到任务队列中,等待主线程通知执行

Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop!

Event loop:客户端必须使用本章节中所描述的事件循环,来协调事件,用户交互,脚本,呈现,网络等等。 事件循环有两种:用于浏览上下文的事件循环和用于 worker 的事件循环。

关于浏览器Event Loop

任务队列分为宏任务队列(macro tasks) 和 微任务队列(micro tasks)

如何判断一段代码是加入到宏任务队列还是微任务队列?
每个任务都由特殊任务源来定义。 来自同一个特殊任务源的所有任务都将发往特定事件循环。所以我们可以按照不同的来源进行分类,不同来源的任务都对应到不同的任务队列中

(macro-task 宏任务)来源:I/O, setTimeout + setInterval + setImmediate, UI renderder ···
(micro-task 微任务)来源:Promise ,process.nextTick ,MutationObserver, Object.observe ···
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. The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed. Microtasks include mutation observer callbacks, and as in the above example, promise callbacks.

看下完整的执行过程:

关于浏览器Event Loop

• 代码开始执行,JavaScript 引擎对所有的代码进行区分。
• 同步代码被压入栈中,异步代码根据不同来源加入到宏任务队列尾部,或者微任务队列的尾部。
• 等待栈中的代码被执行完毕,此时通知任务队列,执行位于队列首部的宏任务。
• 宏任务执行完毕,开始执行其关联的微任务。
• 关联的微任务执行完毕,继续执行下一个宏任务,直到任务队列中所有宏任务被执行完毕。
•执行下一个任务队列。

参考文档:

  1. 并发模型与事件循环
  2. JavaScript 运行机制详解:再谈Event Loop
  3. 什么是浏览器的事件循环(Event Loop)?
  4. 从 薛定谔的猫 聊到 Event loop

相关推荐