查看“Event Loop:事件循环”的源代码
←
Event Loop:事件循环
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
[[category:JavaScript]] == 关于 == 在 MDN 上看到 JavaScript 的[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop 《并发模型与事件循环》],但由于该页面内容过少,实在不变学习。 而网络上搜到的内容大同小异,互相又有不小出入,难以拿捏。 众所周知,JavaScript 是没有所谓源代码的“源代码”,所以这部分内容只能在“规范”文件<ref name="HTML-Standard">参考:[https://html.spec.whatwg.org/multipage/webappapis.html#event-loops 《HTML Standard》:“Event loops”]</ref>了。 * 以下内容参考《HTML Standard》、MDN、与网络内容整理。 == 前置知识 == 见:<big>'''[[浏览器基础:进程与线程]]'''</big> “一个 Tab 页面对应一个'''渲染进程'''”,而“渲染进程中只能存在一个 '''JS 引擎线程'''”(即,'''主线程'''),即:页面中的所有 JS 都由同一单线程运行。 ??????由于“渲染进程”负责了页面几乎所有的任务(事件、交互、脚本、渲染、网络),所以如何协调各个任务的执行是关键问题。 [[File:浏览器的进程与线程.png|400px]] === JavaScript的“任务” === JavaScript中,“任务”被分为两种:“同步任务”,“异步任务”。“异步任务”(Task):又分为“宏任务”(MacroTask)与“微任务”(MicroTask)。 Event Loop 是针对“异步任务”而言。 ——【'''P.S. 《HTML Standard》中已经不再有“宏任务”(MacroTask)这一说法 —— 它就是“任务”(task)'''】 # 同步任务:在“主线程”上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。 # 异步任务:不直接进入“主线程”,而进入“任务队列”(task queue)的任务。 ## '''任务(task)''':事件、解析、回调、使用资源、对 DOM 操作做出反应 ——【根据《HTML Standard》<ref name="HTML-Standard"/>中定义】 ##* 包括:<span style="color: green">'''script(整体代码)、setTimeout、setInterval、setImmediate(node独有)、requestAnimationFrame(浏览器独有)、I/O、UI rendering'''</span> ##* 注意:<span style="color: blue">'''Event Loop 开始时,只有“已经处于 task queues 中”的任务才会被执行'''。</span> ##*: ——【所以,如果一个 setTimeout 引起的无限循环,会导致性能下降,但是很难导致直接崩溃】 ## '''微任务(microtask)''':——【暂无明确定义,见:[[#到底什么是“Microtasks”?]]】 ##* 包括:<span style="color: green">'''Promises 的回调、queueMicrotask()、process.nextTick(node独有)、Object.observe(废弃)、MutationObserver'''</span> ##* 注意:<span style="color: blue">'''Event Loop 开始时,“microtask queue 中的所有”任务会被执行(哪怕是中途添加的“microtask”),直到队列为空'''。</span> ##*: ——【所以,如果一个 Promises 引起无限循环,将直接导致崩溃。这也是为什么要求'''代码慎用“queueMicrotask()”'''】 <big>💡</big>一点小小的猜测: 来源于“渲染进程”中其他线程(非“JS 引擎线程”)引起的异步任务,即“任务(task)”。 来源于“渲染进程”的“JS 引擎线程”引起的异步任务,即“微任务(microtask)”。 === JavaScript的“运行时” === 与 Java 的运行时概念类似,JavaScript 也有其运行时。 == Event Loop<ref name="HTML-Standard"/> == 要协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理必须使用本节中描述的事件循环。 每个“代理”(Agent<ref> <blockquote> “代理”(Agent)包括一组:ECMAScript执行上下文、执行上下文堆栈、运行的执行上下文、代理记录和执行线程。除了执行线程之外,代理的组成部分只属于该代理。 ——《ECMAScript® 2023 Language Specification》 </blockquote> <blockquote> 从概念上讲,代理概念是一个独立于体系结构、理想化的“线程”,JavaScript代码在其中运行。 ——《HTML Standard》 </blockquote> <blockquote> 每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列以及一个微任务队列构成。除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其他组成部分对该代理都是唯一的。 ——MDN:《深入:微任务与 Javascript 运行时环境》 </blockquote> * 一个宿主(浏览器、Node.js)可以有多个 Agent,当两个 Agent 同处一个“Agent Clusters”(代理集群)时,它们可以共享内存。 * Agent 之间相互隔离,可以通过发送消息进行通信。 * 一个 Agent 对应一条“执行线程”。 * 每个 ECMAScript 程序都必须在一个名为 Agent 的区域内执行。 参考: # [https://tc39.es/ecma262/#sec-agents 《ECMAScript® 2023 Language Specification》:“Agents”一节] # [https://html.spec.whatwg.org/multipage/webappapis.html#agents-and-agent-clusters 《HTML Standard》:“Agents and agent clusters”一节] # [https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth MDN:《深入:微任务与 Javascript 运行时环境》] # [https://github.com/Lawliet01/everyone-can-read-spec/blob/main/8.execution-environment.md 【人人都能读标准】8.可视化JavaScript的运行环境:agents、执行上下文、Realm] </ref>)有一个关联的“事件循环”,且该循环对该代理是'''唯一'''的。 # Similar-origin window agent 的事件循环称为“'''窗口事件循环'''”('''window event loop''')。 # Dedicated worker agent、Shared worker agent 或 Service worker agent 的事件循环称为“'''工作事件循环'''”('''worker event loop''')。 # Worklet agent 的事件循环称为“'''工件事件循环'''”('''worklet event loop''')。 * 事件循环不一定对应于实现线程。(例如,可以在单个线程中协同调度多个窗口事件循环) * 一个事件循环具有<span style="color: Green">'''一个或多个“任务队列”(task queues)'''</span>。 ** <q>'''任务队列是“集合”(set),而不是“队列”(queue)''',因为事件循环处理模型从所选队列中获取第一个可运行的任务,而不是将第一个任务出列。</q> ** <q>'''微任务队列(microtask queue)不是任务队列(task queues)'''。</q> * 每个事件循环都有一个当前“正在运行的任务”(currently running task),要么是一个任务,要么是 null。初始为空。 ** 它用于处理可重入性。 * 每个事件循环都有<span style="color: Green">'''一个“微任务队列”(microtask queue)'''</span>,该队列是一个初始为空的微任务队列。 * 每个事件循环都有一个“正在执行微任务的检查点”(performing a microtask checkpoint)的布尔值。该值最初为 false。 ** 它用于防止重入调用。 * 每个窗口事件循环都有一个 DOMHighResTimeStamp 表示“上次渲染机会时间”(last render opportunity time),最初设置为零。 * 每个窗口事件循环都有一个 DOMHighResTimeStamp 表示“上一个空闲周期的开始时间”(last idle period start time),最初设置为零。 === 任务(Tasks) === “任务”(Tasks)包括以下几种: # '''事件'''(Events):在特定 EventTarget 对象上调度 Event 对象,通常由专用的任务完成。 # '''解析'''(Parsing):HTML 解析器标记一个或多个字节,然后处理任何生成的标记,通常是一项任务。 # '''回调'''(Callbacks):调用回调通常由一个专用的任务完成。 # '''使用资源'''(Using a resource):当算法获取资源时,如果获取是以非阻塞的方式发生的,那么一旦部分或全部资源可用,就由任务执行对资源的处理。 # '''对 DOM 操作做出反应'''(Reacting to DOM manipulation):一些元素具有响应 DOM 操作而触发的任务,例如:当该元素被插入到文档中时。 从形式上讲,“任务”(Tasks)具有以下结构: # 步骤(steps):指定任务要完成的工作的一系列步骤。 # '''来源'''(source):用于对相关任务进行分组和序列化。 # 文档(document)与任务关联的文档,对于不在“窗口事件循环”中的任务则为 null。 # 脚本评估环境设置对象集(A script evaluation environment settings object set):用于跟踪任务期间的脚本评估。 根据其源字段(source),每个任务都被定义为来自特定的“任务源”(task source)。对于每个事件循环,每个“任务源”都必须与特定的“任务队列”相关联。 === 通用任务源(task sources) === # '''DOM 操作任务源'''(The DOM manipulation task source):用于对 DOM 操作做出反应的功能,例如在将元素插入文档时以非阻塞方式发生的事情。 # '''用户交互任务源'''(The user interaction task source):用于对用户交互做出反应的功能,例如键盘或鼠标输入。 # '''网络任务源'''(The networking task source):用于响应网络活动而触发的功能。 # '''导航和遍历任务源'''(The navigation and traversal task source):用于对导航和历史遍历中涉及的任务进行排队。 === '''处理模型''' === 只要事件循环存在,它就必须持续运行以下步骤: # 让 oldestTask 和 taskStartTime 为 null。 # 如果事件循环有一个'''任务队列'''其中至少有一个可运行的任务,则: ## 由“实现”(浏览器)定义的规则,选择一个任务队列作为 taskQueue。(“微任务队列”并不是一个“任务队列”,所以不会被选择)——【“选择哪个任务队列”:由浏览器定义(即文档中的“implementation-defined”,客户端软件本身被称为一种“implementation”)决定,这就允许优先选择对性能敏感的任务】 ## 将 taskStartTime 设置为“不安全的共享当前时间”。 ## 将 oldestTask 设置为 taskQueue 中的'''第一个可运行任务''',并将其从 taskQueue 中删除。——【任务队列是“set”,所以并非“出队”】 ## 将事件循环的“正在运行的任务”设置为 oldestTask。 ## 执行 oldestTask 的步骤(Steps)。 ## 将事件循环的“正在运行的任务”设置回 null。 # '''执行微任务检查点''': ## 如果事件循环的“正在执行微任务的检查点”为 true,则返回。——【检查重入】 ## 将事件循环的“正在执行微任务的检查点”设置为 true。——【防止重入】 ## 当事件循环的“微任务队列”不为空时:——【!!!'''不为空将一直重复执行'''!!!】 ### 从事件循环的“微任务队列”中“出队”一个任务作为 oldestMicrotask。 ### 将事件循环的“正在运行的任务”设置为 oldestMicrotask。 ### 执行 oldestMicrotask。 ###* 这可能涉及调用脚本的回调,并最终调用“clean up after running script”步骤,而该步骤的最后一步又将调用'''执行微任务检查点'''。这就是为什么设置“正在执行微任务的检查点”来防止“'''重入'''”。 ### 将事件循环的“正在运行的任务”设置回null。 ## 对于负责的事件循环为该事件循环的每个“环境设置对象”,通知该“环境设置对象”上 rejected 的 promise。 ## 清理索引数据库事务。 ## 执行 ClearKeptObjects()。 ## 将事件循环的“正在执行微任务的检查点”设置为false。 # 将 hasARenderingOpportunity 设置为 false。 # 将现在时刻设置为“不安全的共享当前时间”。 # 如果 oldestTask 不为 null,则: ## ……(省略) # '''更新渲染''':如果这是一个窗口事件循环,则: ## ……(省略) # ……(省略) === 总结 === Event Loop 一次循环的顺序为:<span style="color: blue">'''任务队列的“第一个可运行任务”(宏任务) -> 微任务队列的所有任务(微任务) -> 渲染'''</span> 对于说法:“微任务 -> 渲染 -> 宏任务”,从代码运行结果来看似乎没问题,其实忽略了一点“'''script 脚本本身也是一个任务(宏任务)'''”。 == 补充内容 == 关于 Node.js 中的 Event Loop,暂不了解,有时间再看看其他文章。 参考:[https://juejin.cn/post/6844903764202094606 一次弄懂Event Loop(彻底解决此类面试问题)] === 到底什么是“Microtasks”? === 无论是《ECMAScript® 2023 Language Specification》、《HTML Standard》、MDN,都没有找到关于“宏任务”、“微任务”的定义性描述。 ——《ECMAScript® 2023 Language Specification》:压根就没提到 Microtasks 这个字 ——《HTML Standard》:提到了 Microtask queuing: A microtask is a colloquial way of referring to a task that was created via the queue a microtask algorithm. 微任务是一种口语化的方式,指的是通过“排队微任务算法”创建的任务。 ——【但文档中,与“任务”相比似乎只有“任务源”(task source)不同】 —— MDN:只提到了“任务”(Tasks)与“微任务”(Microtasks)的区别 此外,在 Jake Archibald 的文章<ref>参考:[https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ 《Tasks, microtasks, queues and schedules》——Jake Archibald]</ref>中提到: ECMAScript has the concept of "jobs" which are similar to microtasks, but the relationship isn't explicit aside from vague mailing list discussions. However, the general consensus is that promises should be part of the microtask queue, and for good reason. ECMAScript有类似于 microtasks 的“jobs”概念,但除了模糊的邮件列表讨论之外,这种关系并不明确。然而,普遍的共识是,promises 应该是 microtask queue 的一部分,这是有充分理由的。 …… As mentioned, in ECMAScript land, they call microtasks "jobs". In step 8.a of PerformPromiseThen, EnqueueJob is called to queue a microtask. 如前所述,在 ECMAScript 领域,他们将微任务称为“jobs”。在 PerformPromiseThen 的步骤 8.a 中,调用 EnqueueJob 对 microtask 进行排队。 再此外,javascript.info 中有提到: Microtasks come solely from our code. They are usually created by promises……<ref>参考:[https://javascript.info/event-loop javascript.info:《Event loop: microtasks and macrotasks》]</ref> 微任务仅来自于我们的代码。它们通常是由 promise 创建的…… Asynchronous tasks need proper management. For that, the ECMA standard specifies an internal queue PromiseJobs, more often referred to as the “microtask queue” (V8 term).<ref>参考:[https://javascript.info/microtask-queue javascript.info:《Microtasks》]</ref> 异步任务需要适当的管理。为此,ECMA 标准规定了一个内部队列 PromiseJobs,通常被称为“微任务队列(microtask queue)”(V8 术语)。 所以,Promise 异步任务(注意,仅仅是它的回调,而不是它本身)是 microtask 应该是“潜规则”了,此外,我们还可以使用“'''queueMicrotask()'''”方法来添加 microtask。 : ——【P.S. 对于 Node.js 并不清楚】 === 如何添加 microtask?<ref>参考: # [https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide MDN:《在 JavaScript 中通过 queueMicrotask() 使用微任务》] # [https://developer.mozilla.org/zh-CN/docs/Web/API/queueMicrotask MDN:《queueMicrotask()》] # [https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#microtask-queuing 《HTML Standard》:“Microtask queuing”] </ref> === '''Window''' 或 '''Worker''' 接口的 <span style="color: blue">'''queueMicrotask()'''</span> 方法,将微任务加入队列以在控制返回浏览器的事件循环之前的安全时间执行。 '''语法:''' : <syntaxhighlight lang="JavaScript">queueMicrotask(() => {/* ... */});</syntaxhighlight> '''示例:''' : <syntaxhighlight lang="JavaScript" highlight=""> let callback = () => log("Regular timeout callback has run"); let urgentCallback = () => log("*** Oh noes! An urgent callback has run!"); log("Main program started"); setTimeout(callback, 0); queueMicrotask(urgentCallback); log("Main program exiting"); </syntaxhighlight> : 输出: : <syntaxhighlight lang="JavaScript" highlight=""> Main program started Main program exiting *** Oh noes! An urgent callback has run! Regular timeout callback has run </syntaxhighlight> === 如何实现无延迟的 setTimeout? === == 参考 == <references/>
返回至“
Event Loop:事件循环
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
笔记
服务器
数据库
后端
前端
工具
《To do list》
日常
阅读
电影
摄影
其他
Software
Windows
WIKIOE
所有分类
所有页面
侧边栏
站点日志
工具
链入页面
相关更改
特殊页面
页面信息