事件的循环和异步
2026/7/10 6:05:04 网站建设 项目流程

事件的循环和异步的综述
我们首先需要搞清楚,事件 循环 异步 这三个东西是什么。事件在第一部分开头就讲过了。

而循环和异步,甚至包括事件,有不少朋友都是糅杂在一起讲的。把它们在概念上分清,有助于我们更好的学习。

事件 Event:

事件本身,没有任何执行代码的能力。在第一部分,我们花了很大的篇幅去讲解事件的本体,也就是 C++ 底层那个庞大的结构体,以及在 JS 层包裹它的那个“代理壳”(Proxy Wrapper)。事件的本质,是系统在某个物理瞬间或逻辑节点发出的一个被动信号。

当鼠标在屏幕上精准点击了某个按钮,底层操作系统路由了硬件中断,通过进程间通信通知了浏览器,最终在内存里实例化出了一个 MouseEvent 对象。
这个对象上密密麻麻地盖满了印章(内部插槽):点击的绝对坐标 [[screenX]]、相对视口坐标 [[clientX]]、事件类型 [[type]]: “click”,以及预先计算出来的、锁死在 [[path]] 插槽里的那条从 window 顺流而下再反弹回来的物理传播路径。
这一整套动作,只是在表明了一个事实:有事发生了。

它是一个现场数据载体,但它自己动不了。它躺在内存里,就像是一本写好了“第 3 场第 4 幕,主角被刺”的剧本。至于谁来演、什么时候演、事件自己一概不管,也管不着。

异步 Asynchrony:

很多初学js的朋友以为异步是某种高深的多线程并发技术,其实不然。对于单线程的 JavaScript 来说,异步纯粹是一种在时间维度上的执行策略。

它的核心逻辑用一句话概括就是:“把当下不能立刻完成、或者代价极其高昂的活儿,先在后台登记下来,把当前的主线程执行权让出来,拆给未来去干。”

在传统的同步世界里,执行是死板的。如果有一步需要发起网络请求去获取一个 10MB 的大文件,主线程就必须在原地干等着,直到数据返回。此时整个程序停摆,这就是“阻塞(Blocking)”。

而异步策略则灵活得多:主线程引擎一看,这个网络请求不知道什么时候才能响应。于是它把实际的网络 I/O 工作交给了宿主环境的网络线程,并在通讯录上登记一下:“等网络线程把数据拿回来了,触发这个回调函数。” 随后,主线程立刻转头去执行后面的同步代码。

异步的精髓就在于“发起登记 -> 移交控制权 -> 后台等待 -> 未来触发”。它是一种让资源利用率最大化的智慧,保证了单线程永远在做有意义的运算,而不是死等。

异步主要解决的是I/O 密集型任务的阻塞问题。对于 CPU 密集型任务,单线程的 JavaScript 本身仍然会阻塞,这时候需要使用 Web Worker 等技术。

事件循环 Event Loop:

这是第四部分真正的主角,首先要明确:事件循环,不是 JavaScript 引擎(比如谷歌的 V8)的一部分。

在 V8 引擎源码中,会看到精妙的解释器、优化编译器,以及处理调用栈(Call Stack)的机制。但是,在 V8 里面绝对找不到任何关于 setTimeout 队列、网络请求回调或者事件循环 while 循环的底层实现。

JS 引擎本身,是一个极度纯粹的执行机器。 比如 chrome中的V8 只负责解析、编译和执行 JavaScript 代码,以及垃圾回收和内存管理。所有与外部世界的交互 —— 定时器、网络请求、DOM 操作、事件监听 —— 全部由宿主环境提供。

而js引擎,比如v8,只有看到眼前的同步代码(当前的执行上下文栈)时,才会开始疯狂工作,直到把眼前的逻辑跑完、调用栈彻底清空。一旦栈空了,JS 引擎就会陷入无所事事的休眠状态,它自己既不知道接下来还有没有网络请求要来,也不知道用户有没有点鼠标。

真正让这一切运转起来的,是包裹在 JS 引擎外层的宿主环境(Host Environment)。在浏览器里,这个宿主是 Blink 渲染引擎和底层的多线程架构,在 Node.js 里,则是异步事件驱动库 libuv。

而事件循环,就是宿主环境派驻在 JS 引擎身边的一个“总调度师”。

浏览器作为宿主环境,在 JS 引擎之外维护着多个独立的线程:定时器线程、网络 I/O 线程、UI 渲染线程等等。当外面的底层线程把活儿干完了,总调度师就会把对应的 JS 回调代码塞进他手里的“任务队列”。

总调度师的工作极其机械死板,它持续监控着 JS 引擎的调用栈。只要 JS 引擎一跑完眼前的代码,把调用栈空出来,总调度师就会立刻走过去,翻开任务队列,抽出下一个排在首位的任务,塞到 JS 引擎的执行栈里:“歇够了没?该处理这段逻辑了,马上执行”

注意,这里说的 任务队列 并不是一个单一的队列,而是包含了不同优先级的多个队列,其中最重要的就是我们后面还会详细讲解的宏任务队列和微任务队列,关于任务队列,在第一部分开头也有讲过。

task queue / microtask queue 这里为了方便理解,暂时沿用‘宏任务’和‘微任务’这两个常见说法。

现在,我们将这三个概念放在在一起:

事件 (Event) 负责“宣告发生”,它是底层现场数据的静态快照。
异步 (Asynchrony) 负责“延后交接”,它是跨越时间的执行与状态挂起策略。
事件循环 (Event Loop) 负责“统筹轮转”,它是宿主环境在宏观上掌控的排班排程制度。
它们三位一体,严丝合缝地配合在一起。事件在外部触发,异步将结果包装后在队列里排队,而事件循环在中间掌控传送带的节奏,将一个又一个任务送入 JS 引擎的处理流水线。正是因为宿主环境在外部打好了这套精妙的调度配合拳,JavaScript 这个单线程的执行器,才能在前端庞大、复杂的动态世界里,撑起丝滑流畅的宏大场面。

2.dispatchEvent返回之后
在第三部分的结尾,随着事件派发之车跑完全程,所有的 capture(捕获)和 bubble(冒泡)监听器依次执行完毕,dispatchEvent 函数终于返回了。

但是,事件处理并没有画上句号,从底层系统的视角来看,真正的关键时刻此时才刚刚开始。当同步代码的战斗完成,留在战场上的并不是风平浪静,而是一个需要清理和调度的庞大状态网。

2.1 执行栈 Execution Context Stack 和 微任务检查点

我们先看看主线程的核心工作区域 执行上下文栈 Execution Context Stack,即调用栈。

当事件相关的回调函数被压入调用栈并执行完毕后,调用栈会一层层退散。

但是,dispatchEvent 函数本身的返回,并不绝对等于执行栈彻底清空。这里我们需要精确的描述出三条不同的物理路径,它们在底层的处理逻辑有着本质的区别:

用户真实点击(原生物理交互)
这是纯正的异步调度。当用户在硬件上完成点击,浏览器将其包装成一个宏任务推入任务队列。当这个任务出队并在主线程上执行时,最外层并没有其他 JS 脚本在压着栈。因此,当 dispatchEvent 执行完毕返回、且该任务对应的领域执行上下文(realm execution context)被弹出时,在规范的抽象状态机判定中,JavaScript 执行上下文栈将彻底回归到为空(Empty)的状态。

注:领域realm是规范中的概念,对应一套完整的全局内置对象体系。有助解释不同脚本/模块之间的原型链与全局隔离等高级语义(如 iframe 之间的差异)。大多数情况下,Realm(规范概念) = Context(V8 物理实现)。

脚本触发的点击(调用 element.click())
这是一种“激活行为”,它的本质是一次同步的函数调用。当你在一段正在运行的脚本中写下 btn.click() 时,浏览器会无缝切入事件派发流程。这就表示,当派发结束、click() 返回时,控制权只是交还给了外层那段还没跑完的同步代码。此时,执行栈并没有空,最初触发它的那行脚本依然在栈底压着。

手动派发事件(调用 element.dispatchEvent(event)):
这属于纯粹的合成事件派发(Synthetic Event Dispatch)。它和 click() 类似,同样是完全同步的,会直接在当前的执行栈里压栈执行。但它与 click() 的核心区别在于:element.click() 会同时触发事件派发和浏览器原生默认激活行为(如复选框勾选、链接跳转),而 element.dispatchEvent(event) 仅会执行纯粹的事件派发流程,绝对不会自动触发与该元素关联的原生默认激活行为。

那么,我们为什么要如此精确精准的描述执行栈的清空状态呢?

因为在 WHATWG 规范中,存在一个极其严格的判定关卡——微任务检查点(Microtask Checkpoint)。

微任务(如 Promise.then 的回调、MutationObserver)并不是写下就会立刻执行的。规范给出的底层规则是:微任务的就地清空由 HTML 规范的 Clean up after running script 算法死守。只有当一个独立的回调执行完毕、JavaScript 执行上下文栈彻底为空,且当前未处于“正在执行微任务检查点”的保护状态时,宿主环境才会立刻拉响警报,触发微任务检查点。

一旦满足条件,主线程就会立刻停下宏观的任务轮转,转头去把微任务队列里的所有积压一口气清理干净。
注意,这里规范会开启一个底层的重入保护锁(将 performing a microtask checkpoint 标志位置为 true),这个锁的作用是防止微任务检查点被递归调用,避免出现栈溢出,如果在清空微任务的过程中,又动态追加了新的微任务,它们会被继续挂载到队列末尾并在本次检查点中被强制解决掉,直到队列连个渣都不剩。如果执行栈因为 btn.click() 或手动 dispatchEvent 被外层脚本压住,微任务就只能憋在队列里继续等待。

2.2 其他宏任务正在排队

就在主线程认真的在执行栈里处理 dispatchEvent 的这几毫秒甚至几十毫秒内,外面的世界也是同样在忙碌。

主线程在执行同步代码时,对外界的变化是无法即时响应的。但是,包裹着它的宿主环境(浏览器)是一个庞大的多线程架构。在这个短暂的时间差里,后台的各个线程可能已经发生了许多事情:

网络 I/O 线程刚刚把一张图片下载完,触发了 load 数据的就绪信号。
定时器线程发现一个 setTimeout 的 1000ms 倒计时刚好归零。
用户烦很大的又在键盘上敲了一下,引发了新的输入信号。
这些在后台已经干完的活儿,主线程此时分身乏术。于是,宿主环境会将它们的回调逻辑分别包装成一个个崭新的宏任务(Macrotask),悄悄地塞进对应的任务源(Task Source)队列中,在门外静静地排队等候临幸。

2.3 内存树已变与渲染时机

在刚才的事件回调里,你的代码可能执行了类似 button.style.backgroundColor = ‘red’ 或者 document.body.appendChild(div) 这样的操作。

这里要注意:执行完这些代码的瞬间,屏幕上的颜色并没有变成红色,新的元素也没有立刻出现在显示器上。

主线程对 DOM 的修改,仅仅是同步改变了浏览器内存中逻辑数据树(DOM Tree)和样式规则树(CSSOM)的状态。修改内存是在极短时间内同步完成的,但这并不意味着浏览器会立刻把变化“画”到显示器的像素点上。

从高层流程上理解,页面的视觉更新需要走一遍完整的渲染管线:重新计算样式(Style)、重排布局(Layout)、绘制图层(Paint)以及最终的图层合成(Composite)。

但从底层调度来看,浏览器的渲染是由自身的事件循环和“渲染时机(Rendering Opportunity)”共同决定的。

浏览器非常精明,它通常会结合显示器的刷新率(例如 60Hz 对应约 16.6ms 一帧)来评估性能。如果你在一个宏任务里连续修改了 100 次颜色,浏览器也绝对不会频繁走 100 次渲染管线。它会等待当前的同步任务执行完毕并清空所有微任务,然后在本次事件循环的渲染评估阶段,判定“现在到了适合刷新画面的时机”,才会启动渲染更新。而我们常说的 requestAnimationFrame(rAF),正是在浏览器决定重绘的渲染管线启动前夕、样式计算和布局之前,被集中调用的特权拦截器。

2.4 宿主调度器重新接管

综合以上所有的内部状态,我们可以得出一个至关重要的结论:

当程序的“这一轮”同步逻辑宣告结束时,主线程并不会自己凭空旋转着去寻找下一段代码。 负责执行的底层引擎本质上只是一个高级的运算器,它内部并没有掌控全局的死循环。当微任务打扫完毕、渲染评估完成后,如果没有新的宏任务就绪,JS 引擎就会暂时陷入静默的休眠状态,等待调度器唤醒。

真正让整个网页保持生命力、决定“下一轮”该谁上场的,是宿主环境的调度器(Scheduler)。

在这一刻,调度器重新接管了最高控制权。它严格按照 HTML 标准中规定的事件循环标准处理模型(Processing Model)节拍,开启了一轮极其精准的状态盘点:

宏任务落幕:当前作为运行单元的宏任务彻底宣告终结,并将其从当前执行任务清空。

宏观微任务清算:在宏任务结束后,强制触发一次完整的 Microtask Checkpoint。由于宏任务已结束,此时执行栈必然为空,宿主环境会彻底强行清空整个微任务队列(包括执行期间新产生的微任务)。

渲染时机评估与更新:盘点当前距离上一次屏幕刷新过去了多久。如果刚好命中了硬件的刷新节拍(Rendering Opportunity),则立刻启动渲染更新步骤:依次执行当前的 rAF 回调,并无缝推送页面走完计算样式和重排布局的渲染管线。

下一个宏任务决策:审视各个不同优先级任务源队列(Task Source Queues)的积压情况,根据实现定义的调度策略(现代浏览器通常优先抓取更紧急的用户交互任务以保证响应性),选择最老的可运行任务,重新将其推入 JS 引擎主线程执行。

3.事件循环到底是在循环什么
很多前端初学者在被问到“什么是事件循环”时,脑海中往往会浮现出这样一幅画面:JavaScript 引擎(比如 V8)的底层源码里,写着一个类似 while(true) 的无限循环。它就像一个不知疲倦的纺车,没日没夜地在原地疯狂空转,疯狂地去扫描任务队列里有没有新代码,一旦抓到就立刻烧热 CPU 去执行。

这个认知,在底层逻辑上是完全错误的。

在底层的真实世界中,事件循环不是 JS 引擎内部的空转,而是宿主环境(浏览器 Blink 引擎 / Node.js 的 libuv)精心设计的一套“调度协议”与“状态盘点制度”。 在没有任务处理时,事件循环不仅不会空转,反而会通过底层的操作系统内核(如 Linux 的 epoll、macOS 的 kqueue 或 Windows 的 IOCP)将主线程彻底挂起进入休眠状态,让出 CPU 资源。只有当底层硬件中断、网络数据包到达或定时器到期时,主线程才会被操作系统瞬间唤醒。

3.1 非抢占式调度和调用栈的绝对控制权

要理解事件循环在循环什么,首先要明白主线程在运转时的核心规则:JavaScript 的代码执行是极其依赖执行上下文栈(Call Stack,即调用栈)的。而这个执行机制,是典型的“协作式调度(Cooperative Scheduling)”,即常说的“非抢占式”。

什么是协作式(非抢占式)?

在操作系统的层面,一个线程正在算账,操作系统可以通过硬件中断强行把这个线程掐断,把控制权夺走去干别的,这叫抢占式。但 JavaScript 主线程不吃这一套,它遵循的是运行至完成(Run-to-completion)的特性。

当宿主环境把一段 JS 脚本推上主线程,V8 引擎开始解析并压栈执行的瞬间,在 JavaScript 的逻辑层面,只要当前调用栈中仍有代码正在执行 ,宿主环境就无法插入新的js任务,无法从外部打断它:

哪怕此时用户把鼠标点烂了,引发了海量的物理中断;
哪怕此时网络下载完了一百个文件,急需触发回调;
宿主环境(浏览器)也只能在旁边默默看着,绝对无法在函数执行的中途强行插播代码。
在单线程的限制下,宿主环境想要重新夺回主线程的控制权、去看看“接下来的大局该怎么安排”,唯一的契机,就是当前 JavaScript 执行上下文栈彻底变为空(Empty)的瞬间。(注意:栈空后,引擎会先清空微任务队列,随后控制权才真正交还给下一步调度。)

只有当当前的脚本跑完,最后一个函数弹出调用栈,控制权才会从 V8 引擎的手里交还给宿主环境的调度器(如浏览器的 Blink Scheduler)。在这个绝对空白的转折点上,宿主环境才有机会真正睁开眼睛,开启它的状态盘点。

3.2 循环的本质是“状态盘点”:

事件循环所谓的“一圈”,绝非无脑的代码扫描,本质上是宿主环境在控制权交还的空白间隙,严格按照 WHATWG HTML Living Standard 规范中的事件循环处理模型(Processing Model),进行的一场高密度的状态盘点与大清算。

它手里拿着一张严密的底层清单,按部就班地核对以下四个维度的时空状态:

一 宏任务源盘点(Task Source Check)

在规范的严谨定义中,并不存在一个名叫“宏任务队列”的单一实体。规范使用的核心术语是 任务源(Task Source),例如:DOM 操纵源(DOM manipulation)、用户交互源(User interaction)、网络源(Networking)、导航与遍历源(Navigation and traversal)等。

规范规定:每一个 Task Source 必须关联到一个具体的 Task Queue(任务队列)。但在浏览器具体实现时,为了优化调度,可以将多个不同的 Task Source 塞进同一个物理的 Task Queue 里(例如,将网络任务源与导航任务源合并到同一个物理任务队列中)。

此时,调度器会在这里进行实现定义(Implementation-defined)的优先级裁决。通常情况下,为了保证页面的丝滑响应与防冻结,浏览器会尽量优先处理用户交互相关任务。

调度器会首先以实现定义的方式从多个任务队列中选出一个队列,然后从该特定队列中取出最老的一个可运行任务(Runnable Task),将其推入主线程的调用栈。
规范在此处有一个硬性限制:无论采用何种优先调度策略,同一任务源(Task Source)内部的任务顺序,绝对不可被打乱。

二 微任务全面清算(Microtask Checkpoint)

当上一步的宏任务执行完毕,调用栈再次归零的瞬间,调度器会立刻切入微任务检查点(Microtask Checkpoint)。

这里要注意,微任务检查点不仅会在任务执行结束后触发。根据 HTML Standard 中的 Clean up after running script 算法,在脚本或回调函数执行完毕且判定执行栈为空时,同样会触发微任务检查点。

在这一步中,调度器会盯着事件循环的微任务队列(Microtask Queue),开始清空。

这个盘点是非常彻底的:它循环从队列中取出最旧的微任务交由引擎执行,直到队列为空。如果在清空微任务的过程中,动态追加了新的微任务,它们会被继续挂载到当前队列末尾,并在本次检查点中被继续执行。

前面也讲过了,这里再次重复一下,规范通过将 performing a microtask checkpoint 标志位置为 true(重入保护锁),防止了清空算法在嵌套执行时被重复进入。但这并不影响当前正在运行的微任务清空循环:如果在执行过程中动态追加了新的微任务,它们会被挂载到队列末尾,并在当前循环中立即执行,直到队列彻底清空。

三 渲染时机评估(Rendering Opportunity)

微任务彻底打扫干净后,主线程将迎来事件循环中最精明的一个阶段。调度器会开始盘点时间的流逝:
“当前文档是否迎来了渲染时机(Rendering Opportunity)?”

需要注意,是否渲染是针对每个文档(Document)和可导航上下文(Navigable)独立评估的,绝不是事件循环每转一圈就盲目重绘一次。调度器在宏观上会进行多维过滤:

目标文档当前是否处于可见状态(例如 visibilityState 是否为 hidden)?
距离上一次渲染是否过于接近,尚未到达显示设备的下一次刷新节拍?
本轮更新是否根本不会产生任何可见效果?
注:JavaScript 的执行速度极快,事件循环(Event Loop)一秒钟可能转了几千圈。但是,绝大多数普通显示器的硬件刷新率是 60Hz(每秒刷新 60 次),也就是大约每 16.6 毫秒屏幕才会物理重绘一次。

如果评估结果判定当前不适合进行渲染更新,调度器会直接跳过本次视觉渲染流程,迅速开启下一轮任务提取。反之,如果浏览器判断当前存在渲染时机,它就会进入规范中的 Update the rendering 流程。

关于渲染更新的内容,我们在后面将专门用一小节来进行详细讲解。

四 空闲节拍盘点(Idle Period)

如果当前窗口的事件循环中已经没有任何可运行的任务(No Runnable Task),意味着主线程在这一刻彻底闲了下来。
面对下一个硬件刷新信号到来前的这段珍贵间隙,且浏览器判定当前适合执行低优先级工作时,调度器便会开启空闲周期(Idle Period)。

在进入空闲周期前,调度器会利用标准算法,盘点出一个极其精确的剩余时间截止牌(Deadline)。这个 Deadline 的计算极为严格:

它首先给出一个默认上限——“当前时间 + 50ms”,以确保一旦用户突然产生新的输入,主线程能够在合理时间内迅速响应;
随后,它会去翻看定时器账本,找出所有待处理定时器中最早到期的那个时间点;
如果此时还存在即将到来的渲染机会,它还会结合下一次渲染节拍进一步收紧时间预算。
调度器会综合这些时间点,并取其中最早的那个作为最终 Deadline。最终计算出的安全截止时间会被注入到 IdleDeadline 对象中。

调度器带着这个截止牌,开始按序调用那些排在空闲队列里的 requestIdleCallback(rIC)回调。回调函数中的代码可以通过 deadline.timeRemaining() 实时查看自己还剩余多少可用时间。一旦发现时间预算即将耗尽,就应主动结束当前工作并交还控制权,从而确保这些低优先级后台任务不会拖累高优先级的用户交互与流畅的动画渲染。

这里我们需要详细的说一下,在某一轮事件循环中,当微任务被清空后,进行渲染更新的判断,当判断结果为不需要,那么就继续事件循环,看任务队列取任务,如果队列中没有可执行的任务了,那么就进入空闲周期,首先给出初始的50毫秒,然后看是否有定时器,假如没有定时器,那么就看是否有待定渲染 pending render,假如有,那么就看下依次渲染边界,在60赫兹下,是16.7毫秒。

可能有朋友会疑惑,在进入空闲周期之前,渲染更新也判断过了,队列里也没有可执行任务了,这才刚进空闲周期,啥也没干呢,怎么就又要判断是否有待定渲染? 这个待定渲染是什么?

在微任务清空后,update the rendering 判断的是本轮次循环的渲染机会,就是要不要真的更新渲染。

进到空闲周期以后, **pending render 是在问 **“同一个事件循环里,是否还存在需要保留渲染边界的页面/窗口状态”,在规范层面,一个“Window event loop”是可以服务于多个“Document”。

比如,主页面 A.com 中嵌入了一个同源的 iframe B.com,那么很大可能,它们就是同享一个主线程,同一个事件循环。

浏览器通过自己对待定渲染的判断,加上是否有定时器,然后把可用的空闲周期的时间Deadline,从最初的50毫秒,按照最小的时间收紧。

假如,页面完全静止 无定时器 无任何待定渲染,空闲周期为50毫秒。

假如,刚刚渲染完毕,就进到了空闲周期,60赫兹下,此时距离下次硬件刷新还剩16.7毫秒,

无定时器,浏览器判断有待定渲染,那么,空闲周期为16.7减去1,15.7毫秒的空闲周期,减去的1毫秒,为渲染准备时间,规范没说具体值,但一般实现 比如chrome 都是使用1毫秒的渲染准备时间。

假如,60赫兹下, 距离下次硬件刷新还剩8毫秒,进到了空闲周期,无定时器,浏览器判断有待定渲染,那么,空闲周期为8-1=7毫秒。

最后,是一个边界情况,假如最后算出的Deadline小于等于0,那么就直接跳过了空闲周期。

至此,事件循环的一次完整“盘点”便宣告结束。
它并不是一个在后台疯狂旋转、不断扫描代码的无脑 while(true),而是一套由任务提取、微任务清算、渲染评估以及空闲调度共同组成的精密状态机。每当调用栈清空、控制权重新回到宿主环境手中时,这套盘点制度便会再次启动,周而复始地维持整个 Web 世界的运转。

3.3 僵尸的入土时机:

我们在第三部分讲解 removeEventListener 的底层原理时,我们提到过一个辣手的并发场景:假设在一个事件派发(Dispatch)的遍历执行过程中,前一个回调函数竟然把紧排在它后面的监听器给注销了,这会引发致命的数组遍历索引越界(Concurrency Bug)。

为了解决这个问题,浏览器底层采用的是一种极其克制的“软删除”策略:当你在 JS 代码里调用 removeEventListener 时,底层的 C++ 引擎并没有立刻把那个监听器项从内存数组里物理剔除。它只是温柔地在那个表项上打了一个勾:removed: true。

这种软删除策略虽然完美保障了正在执行的事件派发不被打断,但也留下了一个悬念:这些已经在逻辑上宣告死亡的“僵尸监听器”,它们极其厚重的 C++ 结构体、JS 包装壳以及缠绵在一起的闭包(Closure)上下文,到底在什么时候才能真正入土为安,释放内存?

这就是一场跨越两大引擎的精密联动。

第一步:派发落幕与宿主的“名册清洗”

有些初学的朋友误以为这些标着 removed: true 的僵尸会一直赖在监听器列表中,直到触发垃圾回收。事实并非如此。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询