一直以来,我对事件循环相关的知识都是一知半解,所以前段时间想去学习一下这些不解的地方。同时这是我作为一个小白头几次写文章,可能会有语病和错误,希望大家看到不对的地方在评论里积极指正~ 感谢
我的疑惑和不解
- 什么是事件循环?
- 之前了解到过宏任务(Macrotask)以及微任务(Microtask)的概念,它们是什么?
事件循环
这个部分主要基于两个官方网站的资料:
定义
-
MDN的定义
JavaScript has a runtime model based on Event Loop, which is responsible for executing the code, collecting and processing events, and executing queued sub-tasks.
JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
-
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as descried in the section. Each agent has an associated event loop, which is unique to that agent.
为了协调事件,用户交互,脚本,渲染,网络等,用户代理(比如浏览器)必须按本节所说的方式使用事件循环。每一个代理都有一个关联的、且唯一的事件循环。
理论模型
图像化
组成(来自MDN)
这一部分MDN上写的比较清楚,这里就大致介绍一下。
- 栈(stack)
- 用来存帧(frame)
- 帧是对函数参数和局部变量的引用
- 堆(heap)
- 对象储存于堆中,指示了内存中的一大块区域
- 大部分是非结构化的(unstructured)
- 队列(queue)
- 消息队列(message queue)
- 每个消息都有一个关联的函数用来处理这个消息
个人对于这部分的简单理解是:当有任务出现时,会被放入消息队列。然后运行时再一个个从队列中取出,消息相关的函数运行时会被压入栈中,结束运行时会弹出。
组成(来着whatwg HTML Standard)
- task queue/任务队列
- 一个事件循环有一个或多个任务队列。
- 对于每个事件循环,每个任务来源(task source)一定要和一个特定的任务队列关联。
- currently running task/当前运行的任务
- task or null/任务或者null。
- 初始为null
- microtask queue/微任务队列
- performing a microtask checkpoint boolean/执行微任务检查点
- 初始为false
- last render opportunity time
- 初始为0
- 暂时没有深入研究用处,感兴趣的可以去网站继续深入阅读~
- last idle period start time
- 初始为0
- 暂时没有深入研究用处,感兴趣的可以去网站继续深入阅读~
这里提到的带有超链接的词,在后面都会有解释,可以继续阅读下去,或者点击跳转查看。
其他相关的定义(来自whatwg HTML Standard)
task queue/任务队列
- 任务队列是任务的集合(Set)
- 注意点
- microtask queue is not a task queue/微任务队列不是一个任务队列。
- 任务队列是Sets,不是Queues。
-
原因:
because step one of the event loop processing model grabs the first runnable task from the chosen queue, instead of dequeuing the first task.
因为事件循环处理模型的步骤一是从所选队列中抓取第一个可运行的任务,而不是出列第一个任务。
如果使用真正的队列(queue),获取到的可能就不是runnable的任务。
-
task/任务
-
定义
Tasks encapsulate algorithms that are responsible for such work like below
任务封装了对工作负责的算法
-
任务所负责的工作
- Events/事件
- Parsing/解析——HTML parser/HTML解析器
- Callbacks/回调
- Using a resource/使用资源
- Reacting to DOM manipulation/对DOM操作做出的反应
-
任务的结构
-
Steps/步骤
A series of steps specifying the work to be done by the task.
制定任务要完成的工作的一系列步骤。
-
A source/一个来源
one of the task sources, used to group and serialize related tasks.
是任务源中的一个,任务源是用来对相关任务进行分组和序列化。
注意点:
Per its source field, each task is defined as coming from a specific task source. For each event loop, every task source must be associated with a specific task queue.
Essentially, task sources are used within standards to separate logically-different types of tasks, which a user agent might wish to distinguish between. Task queues are used by user agents to coalesce task sources within a given event loop.根据任务的源字段,每个任务都定义为来自特定的源。对于每个事件循环,每个任务源都必须与特定的任务队列相关联。
从本质上讲,任务源在标准中用于分离逻辑上不同类型的任务,用户代理(比如浏览器)可能希望区分这些任务。在一个事件循环中,用户代理使用任务队列来合并任务源。
个人理解:这也是为什么可能会有多个任务队列的原因,一个用户代理可能希望区分不同类型的任务,然后以不同的方式处理他们。同时用户代理会使用任务队列(每个任务源关联一个任务队列)来合并它们。
-
A document/一个文档
A Document associated with the task, or null for tasks that are not in a window event loop.
与任务相关的文档,对于那些不在window事件循环中的任务来说是null。
-
A script evaluation environment settings object set/脚本评估环境设置变量集
A set of environment settings objects used for tracking script evaluation during the task.
在任务中用于追踪脚本评估的环境设置对象的集合。
-
-
什么是runnable/可运行的任务
A set of environment settings objects used for tracking script evaluation during the task.
在任务中用于追踪脚本评估的环境设置对象的集合。
宏任务(Macrotask)以及微任务(Microtask)
什么是宏任务(Macrotask)?什么是微任务(Microtask)? 前面MDN以及whatwg HTML Standard中似乎都没有提到宏任务这个词,那这个词是从哪里来的呢?对我来说,一些名词的不确定是我对这一部分的内容感觉到困惑的很大原因之一。。
首先要感谢以下文章,让我对这一部分有了更清晰了了解:
- 理解 JavaScript 中的 macrotask 和 microtask
- Tasks, microtasks, queues and schedules
- What are the microtask and macrotask within an event loop in JavaScript ?
宏任务是什么?
本质上宏任务就是whatwg HTML Standard中定义的任务,macrotask queues也就是task queues。
Macro-tasks include parsing HTML,generating DOM,executing main thread JavaScript code and other events such as page loading,input,network events,timer events,etc.
examples:setTimeout, setInterval, setImmediate,requestAnimationFrame,I/O,UI Rendering.
宏任务包括了解析HTML,生成DOM,执行主线程js代码以及其他如页面加载,输入,网络,时间等事件。
例子:
- setTimeout
- setInterval
- setImmediate(非标准: 该特性是非标准的,请尽量不要在生产环境中使用它!)
- requestAnimationFrame
- I/O
- UI rendering
我们可以发现这和whatwg HTML Standard中对任务的定义非常契合。
微任务是什么?
首先看whatwg HTML Standard定义的微任务:
A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm.
微任务是对通过入列微任务算法创建的任务的口语化指代。
(似乎它说了废话),我觉得比较重要的是在什么时候调用这个算法?对哪些任务调用这个算法? 通过全局查找,我似乎没有在whatwg HTML Standard找到答案。
我又在MDN和Tasks, microtasks, queues and schedules上找到了它们对微任务的定义:
MDN: 一个 微任务(microtask)就是一个简短的函数,当创建该函数的函数执行之后,并且 只有当 Javascript 调用栈为空,而控制权尚未返还给被 user agent 用来驱动脚本执行环境的事件循环之前,该微任务才会被执行。
Tasks, microtasks, queues and schedules: 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.
微任务通常是在当前脚本执行之后需要立即执行的东西,比如对一系列动作做出反应,又或者在不承受宏任务缺点的情况下使用异步。
结合这些网站找到的定义,我大致写了一个自己的理解:微任务就是在其他任务(包括宏任务和微任务)运行的过程中创建的简短函数,它们会被入列微任务算法加入微任务队列。这些简短函数是需要再当前任务执行完毕之后立即执行的,如果不使用微任务,而是使用宏任务去完成,可能需要等待很久才会去处理。
例子:
- Promise.then,catch,finally
- MutationObserver
- process.nextTick
- queueMicrotask: 为了允许第三方库、框架、polyfills 能使用微任务,Window对象暴露了 queueMicrotask() 方法
宏任务和微任务的运行机制
本部分来自Tasks, microtasks, queues and schedules,网站上有详细的可视化演示~
先来看一段代码:
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');
它的输出是:
script start
script end
promise1
promise2
setTimeout
它的运行机制如下:
- 首先先会去取一个宏任务(这段script脚本也是个宏任务)运行。
- 运行script,首先遇到
console.log('script start');, 输出script start。 - 然后运行到
setTimeout,setTimeout的callbacks会被当做新的宏任务加入任务队列。 - 然后运行到
Promise.resolve().then,Promise的callbacks会被当做新的微任务加入微任务队列。这里的第一个Promise then会被加入微任务队列,第二个还没运行到,所以不用管。 - 然后运行到
console.log('script end');, 输出script end。到这,script运行完毕,这个宏任务执行完毕。 - 在一个宏任务结束之后,开始处理微任务队列。首先是运行第一个
Promise then,输出promise1。这个Promise callback返回了undefined,它会将第二个Promise then加入微任务队列。 - 【这里要说明的是:任何在微任务运行过程中新添加的微任务都会被处理,直到微任务队列为空。】
- 所以第二个
Promise then接着运行,输出promise2。微任务队列为空。 - 【在上一步之后,表明这个宏任务(包括相应的微任务)已经运行完毕,那么浏览器可能会去更新渲染】
- 然后再去取一个宏任务,也就是第三步
setTimeout加入的宏任务,运行输出setTimeout。 - 接下来就是按照上面步骤持续的运行。
这就是他们的运行机制啦,想要进一步了解的话可以继续往下看,进一步了解事件循环的处理模型。
Processing model/处理模型
An event loop must continually run through the following steps for as long as it exists.
一个事件循环在存在的过程中会持续的运行如下的步骤:
- 将这个事件循环其中一个任务队列赋值给taskQueue,它是通过实现定义(implementaion-defined)模式选择,这个模式限制了所选的任务队列一定要包含至少一个可运行的任务(runnable task)。如果没有这样的队列,那么就跳到下面的微任务步骤。
- oldestTask = taskQueue里的第一个可运行任务(runnable task),然后从taskQueue里移除这个任务。
- 事件循环的当前正在运行任务(currently running task)= oldestTask。
- taskStartTime = 不安全共享的现在时间(unsafe shared current time)。
- 执行oldestTask的步骤。
- 事件循环的当前正在运行任务(currently running task) = null。
- 微任务:执行微任务检查(perform a microtask checkpoint)。
- hasARenderingOpportunity = false。
- now = unsafe shared current time。
- 报告任务的持续时间(原文有详细步骤,这里就不细写了)
- 更新渲染:如果这个事件循环是window事件循环的话,就执行以下步骤(原文有详细步骤,这里也不详细写了,大致看了一下,是更新渲染的具体步骤的介绍。)
- 如果以下判断全部为true,则执行一些步骤。(原文较为复杂,个人暂时觉得没有必要了解的这么详细,就先跳过了)。
- 如果这是一个worker事件循环的话,就执行以下步骤(也不详细了解,这大致是对于Web workers所需要执行的特殊步骤)。
大致看了一遍下来,其实大部分内容也记不住,不过还是可以大致了解到事件循环所使用模型的大致流程,还有也知道了微任务在其中的位置。这和前面说的宏任务微任务的运行机制其实是大致相同互相契合的,但这段并没有说微任务队列是如何执行的。
所以最后让我再了解一下:执行微任务的检查(performing a microtask checkpoint),它就是负责微任务队列执行的算法。
Perform a microtask checkpoint/执行微任务检查
- 如果当前事件循环的 正在执行微任务检查标志(performing a microtask checkpoint flag) 是true,那么直接返回。(防止重复进入)
- 将当前事件循环的正在执行微任务检查标志(performing a microtask checkpoint flag)设置为true。
- 当此事件循环的微任务队列(microtask queue)不是空的:
- oldestMicrotask = 出列(dequeuing)当前事件循环微任务队列的的一个微任务。
- 当前事件循环的正在运行任务(currently running task)= oldestMicrotask。
- 运行odestMicrotask。
- currently running task = null。
- 对于那些负责的事件循环是当前事件循环的环境设置对象(environment settings object),通知它们关于拒绝(rejected)的promises。
- 清空Indexed Database transactions。
- 执行 ClearKeptObjects()。
- 设置当前事件循环的performing a microtask checkpoint = false。
看到这,我们可以发现微任务队列的运行机制正如上面的例子所示,会一直持续运行到微任务队列为空,就算中途入列一些额外的微任务。
这就是这篇文章的全部啦,感谢阅读~