共计 4684 个字符,预计需要花费 12 分钟才能阅读完成。
Event loop
Javascript 基于 Event Loop 的运行形式不同凡响,microtask queue、task queue 让很多人蛊惑代码为什么这样运行。这里就来看一眼 Event Loop。
Event loop 的执行过程
ECMA-262
作为 Javascript 背地的规范,ECMA-262 里没有定义 Event Lopp,没有 microtask queue 与 task queue。(ECMA-262 同样也没有定义 setTimeout
等定时函数,也没有 I/O。)
它定义了 Job 与 Job queue,并有一个顶层的 [RunJobs] 操作:
- Perform ? InitializeHostDefinedRealm().
In an implementation-dependent manner, obtain the ECMAScript source texts (see clause 10) and any associated host-defined values for zero or more ECMAScript scripts and/or ECMAScript modules. For each such sourceText and hostDefined, do
- If sourceText is the source code of a script, then
i. Perform EnqueueJob(“ScriptJobs”, ScriptEvaluationJob, « sourceText, hostDefined »).- Else sourceText is the source code of a module,
ii. Perform EnqueueJob(“ScriptJobs”, TopLevelModuleEvaluationJob, « sourceText, hostDefined »).Repeat,
- Suspend the running execution context and remove it from the execution context stack.
- Assert: The execution context stack is now empty.
- Let nextQueue be a non-empty Job Queue chosen in an implementation-defined manner. If all Job Queues are empty, the result is implementation-defined.
- Let nextPending be the PendingJob record at the front of nextQueue. Remove that record from nextQueue.
- Let newContext be a new execution context.
- Set newContext’s Function to null.
- Set newContext’s Realm to nextPending.[[Realm]].
- Set newContext’s ScriptOrModule to nextPending.[[ScriptOrModule]].
- Push newContext onto the execution context stack; newContext is now the running execution context.
- Perform any implementation or host environment defined job initialization using nextPending.
- Let result be the result of performing the abstract operation named by nextPending.[[Job]] using the elements of nextPending.[[Arguments]] as its arguments.
- If result is an abrupt completion, perform HostReportErrors(« result.[[Value]] »).
这里,首先依据脚本的类型,将 ScriptEvaluationJob 或 TopLevelModuleEvaluationJob 退出 ScriptJob
队列。而后开始循环解决每一个非空 Job queue。
在同一个 Job queue 外部,工作是严格先进先出的。然而,抉择哪一个是实现决定的。然而,ECMA-262 仅定义了两个 Job queue,一个是下面的顶层的 ScriptJob
,只用于放(惟一一个)顶层工作;另一个是 PromiseJob
,用于 Promise。这两个 Job queue 不会同时非空,因此执行程序其实是确定的。
EnqueuJob 被用于想 Job queue 退出 Job。
HTML
在 HTML 中应用的 Javascript,并没有应用下面提到的 RunJobs,以及 Job queue,而是定义了本人的 Event Loop,以及 task queue,microtask queue。
HTML 的 Event Loop 执行如下的操作:
- Let taskQueue be one of the event loop’s task queues, chosen in a user-agent-defined manner, with the constraint that the chosen task queue must contain at least one runnable task. If there is no such task queue, then jump to the microtasks step below.
- Let oldestTask be the first runnable task in taskQueue, and remove it from taskQueue.
Report the duration of time during which the user agent does not execute this loop by performing the following steps:
- Set event loop begin to the current high resolution time.
- If event loop end is set, then let top-level browsing contexts be the set of all top-level browsing contexts of all Document objects associated with the event loop. Report long tasks, passing in event loop end, event loop begin, and top-level browsing contexts.
- Set the event loop’s currently running task to oldestTask.
- Perform oldestTask’s steps.
- Set the event loop’s currently running task back to null.
- Remove oldestTask from its task queue.
- Microtasks: Perform a microtask checkpoint.
- Let now be the current high resolution time. [HRT]
Report the task’s duration by performing the following steps:
- …
- 以下省略
留神,microtask queue 不是 task queue。task queue 不是队列,因为它不是先进先出的。从第一步能够看出从 task queue 中取出的工作并不一定是最先进入 task queue 的工作。
在 Event Loop 中,实现一个 task queue 的工作之后,会 Perform a microtask checkpoint :
- If the event loop’s performing a microtask checkpoint is true, then return.
- Set the event loop’s performing a microtask checkpoint to true.
While the event loop’s microtask queue is not empty:
- Let oldestMicrotask be the result of dequeuing from the event loop’s microtask queue.
- Set the event loop’s currently running task to oldestMicrotask.
- Run oldestMicrotask.
- Set the event loop’s currently running task back to null.
- For each environment settings object whose responsible event loop is this event loop, notify about rejected promises on that environment settings object.
- Cleanup Indexed Database transactions.
- Set the event loop’s performing a microtask checkpoint to false.
在这里,将会依照先进先出的程序,执行所有 microtask queue 中的工作(包含在此过程中新进入 microtask queue 的工作),直到 microtask queue 为空。
所以执行过程是,执行一个 task queue 的工作,而后执行 microtask queue 中的所有工作,而后进入 Event Loop 下一个循环,再执行一个 task queue 中的工作。
ECMA-262 中由 EnqueuJob 退出的 Job,在 HMTL 中全副进入 microtask queue).
如果仅有由 Promise 生成的 microtask 的话,上述的执行过程与 RunJobs 根本是统一的。所以当前的探讨,都基于 HTML 的 Event Loop 与 microtask / task 的定义进行。
Task vs Microtask
那么,microtask 跟 task 都各有那些呢?这里挑一部分来介绍,应该能够解决很多网上的“为什么输入会是这样”的疑难。
Microtask
所有 ECMA-262 中的 Promise 相干的 Job。包含:
then
、catch
的回调- 如果 Promise 在调用
then
或catch
时曾经 settle(状态已确定),那么相应的回调函数间接退出 microtask queu,参见 PerformPromiseThen。否则,回到被记录,并在 Promise settle 时,通过 TriggerPromiseActions 退出 micro task queue。 await
是由 Promise 实现的,每一个await
,都会通过 Promise.then 执行 await 完结之后的操作(即便await
的对象不是 Promise)。(参见 Await)
- 如果 Promise 在调用
由一个 Promise (P1) 去 resolve 另一个 Promise(P2)的时候,主动生成的一个对
P2.then
的调用。该调用会被退出 microtask queue。- 参见 Promise Resolve Functions
P2.then
执行后,会使P2.then
的回调成为另一个 microtask- 该
P2.then
的回调,将 resolve 或 reject P1
- Mutation Observers
Task
SetTimeout
、SetInterval
的回调。即便工夫设置为0
。- 参见 timer initialisation steps
- 回调会在延时实现后被退出 task queue。