重读event loop

重读event loop

Event Loop

Event Loop 是javascript中的比较重要的一个知识点,在一些基础面试中也比较常见。

中文名:并发模型与事件循环MDN是这样翻译的)

JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。

还配了一张图:

desc

有两个知识点得提前了解一下:堆和栈,涉及到一些数据结构、内存管理等的相关知识。网上相关资料一大把,可自行搜索。

MDN上见面描述一下:

栈(stack)

函数调用形成了一个由若干帧组成的栈。

堆(heap)

对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。

队列

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

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

以上是MDN的描述

这一点其实挺重要的,对于理解 setTimeout(fn, 0) 是不是立即执行有很大帮助。

单线程

大家都知道 javascript 的单线程(single threaded runtime)的,所有的代码都会在栈(stack)被执行,而且一次只会执行一个程式码片段(one thing at a time)。

栈的作用

就像上面图中一样,基本上,一上来就被执行的代码,被放到了最下面(最先压入栈),中间有其他函数被执行的画,会被一次推到栈中,这样,最后最后执行的,一定会在最上面,也是执行之后,最先被移出的。

Youtube的一个视频形象的解释了这个过程:https://youtu.be/2ZH_1d8TYVg。

来一张图可以参考一下
stac

事件循环

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

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

queue.waitForMessage() 会同步地等待消息到达(如果当前没有任何消息等待被处理)。

以上是MDN的一段简单的描述。

再来一张图吧

evenloop

队列(Call Queue)是关键点之一, 里面都是待处理(执行)的函数, 就像代码写的一样,就是个无限循环的过程,只要队列里面有东西,就是被推入到 stack 中执行,这其中关联的对象,数据等等;这个过程是靠引擎来处理的,js只负责执行就行了。引擎来调度。

这里细分的话,又分为宏任务(macro task)、微任务(micro task)。文章最后面有这种任务的介绍。

一个 event loop 都有一个 micro task queue

一个 event loop 总会有一个正在执行任务,而这个任务最开始就是从 macro task 队列中来的, macro task 就是所说的 task queue.

常见的一个 script 标签里面的所有代码,加载之后就是一个 task, 被放入到队列中了,

微任务队列可以在任何一个宏任务之后执行,也就是说,每当结束一个宏任务后都会检测是否有待执行的微任务。并且会一次性执行完微任务队列里的所有微任务,如果在执行的过程中又产生了新的微队列,则继续执行新的微队列,直到队列清空。

浏览器执行顺序小结

浏览器大概的执行顺序如下:

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. 执行UI渲染,然后在跑回来继续执行js;
  7. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  8. 执行完毕后,调用栈Stack为空;
  9. 重复第3-8个步骤;
    x. 重复第3-8个步骤;
    ……

可以看到,这就是浏览器的事件循环Event Loop

这里归纳3个重点:

  • 宏队列macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  • 微任务队列中所有的任务都会被依次取出来执行,之道microtask queue为空;
  • UI渲染是在每次执行完微任务队列后,下一个宏任务之前进行执行。

task & microTask 宏任务、微任务

以下是 task 和 microTask 的分类,规范中有明确的写道,比如 MutationObserver,在这里引用 Promise A+ 规范翻译 中的分类:

task

task 主要包含:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

这个任务(比如,Document 的计时器产生的回调函数,Document 的鼠标移动产生的事件,Document 的解析器产生的 tasks)会由相同 event loop 管理的 task 都必须加入到同一个 task queue 中,可是来自不同 task sources 的 tasks 可能会被排入到不同的 task queues 中。

一个进程中,事件循环是唯一的,但是任务队列可以拥有多个,也就是说,管控全局的只有一个,相当于门将只有一个,但是在场内踢球的的有好多。

来自相同的 task source 的 task 将会被排入相同的 task queue,但是规范说来自不同 task sources 的 tasks 可能会被排入到不同的 task queues 中,也就是说一个 task queue 中可能排列着来自不同 task sources 的 tasks,但是具体什么 task source 对应什么 task queue,规范并没有具体说明。

关于 任务源(task source)可参考标准的解释。

microtask

microtask 主要包含:

  • Promises(这里指浏览器实现的原生 Promise)
  • Object.observe(已被 MutationObserver 替代)
  • MutationObserver
  • postMessage

Web APIs

在上一章讲讲到了用户代理(宿主环境/运行环境/浏览器)来控制任务的调度,task queues 只是一个队列,它并不知道什么时候有新的任务推入,也不知道什么时候任务出队。event loop 会根据规则不断将任务出队,那谁来将任务入队呢?答案是 Web APIs。

我们都知道 JavaScript 的执行是单线程的,但是浏览器并不是单线程的,Web APIs 就是一些额外的线程,它们通常由 C++ 来实现,用来处理非同步事件比如 DOM 事件,http 请求,setTimeout 等。他们是浏览器实现并发的入口,对于 Node.JavaScript 来说,就是一些 C++ 的 APIs。

这张图更明确一点:
event loop

作者

Fat Dong

发布于

2022-03-24

更新于

2022-03-24

许可协议