Roblox Guidebook知识库
简体中文 ⌄

开发 / ROBLOX

Roblox事件连接:避免每次打开菜单都多添加一个监听器

跟踪一条有明确归属的订阅,从创建、重复打开,到断开与重新建立。原创Studio实验比较三个活动监听器和一个受管理连接,并单独验证Once。

更新日期:

先选择一个要调查的错误机制 #

想象一个临时帮助面板,它会对某个事件作出反应。控制器每次打开面板时连接一个新函数,却保留原来的订阅。打开几次之后,一个事件可能调用多个函数。这是一种可能的错误机制,不是对所有现有按钮的诊断。先找出哪个函数创建订阅,以及哪个组件负责结束它的工作。

本练习没有创建真实菜单,也没有点击它的按钮。openPanel与closePanel代表一个假想控制器的生命周期,独立的BindableEvent提供信号。因此我们可以在不制作界面、不修改已发布游戏的前提下检查连接归属。即使这组原创assertions已经通过,真实面板的测试仍然是单独的集成阶段。

区分事件和它的连接 #

官方文档把Connect描述为将函数连接到事件的方法,它返回RBXScriptConnection对象。保留这个对象的引用,就能通过Disconnect断开该订阅。信号、回调函数和返回的连接对象不是同一个东西。只保存函数本身的变量,不会自动变成对连接对象的引用。

在面板练习中,控制器拥有currentConnection。它知道订阅在哪里创建,也在关闭时使用同一条保存的引用。不要把创建操作分散到彼此无关的函数中,却没有明确负责人。这个约定只需要一个字段:保存当前连接,或者在清理后成为nil。这样整个责任链都可以从开始追踪到结束。

一条有明确归属的订阅打开大图 ↗
原创订阅归属示意图。

重现三个多余监听器 #

原创测试的第一部分为同一个BindableEvent创建三条连接。每个回调都增加共享计数器hits。随后只触发一次事件。等待处理后,hits应当等于三。这个结果验证了在该独立场景中,三个活动订阅确实产生三个回调。它并没有声称重现某款现有游戏里的故障按钮。

把每个返回的连接保存在duplicates表里,方便比较完成后断开所有创建的监听器。这个基线使未管理与受管理设计之间的区别能够重复验证。不要把订阅数量写成玩家点击次数。这里有一次事件触发,以及三个分别连接的函数;它们是含义不同的两个计数。

-- Original isolated engine experiment, not an existing-game script.
local signalOwner = Instance.new("BindableEvent")
local hits = 0
local duplicates = {}
for i = 1, 3 do
    duplicates[i] = signalOwner.Event:Connect(function()
        hits += 1
    end)
end
signalOwner:Fire()
task.wait()
assert(hits == 3, "Three live subscriptions must make three callbacks")
for _, connection in duplicates do
    connection:Disconnect()
    assert(connection.Connected == false)
end
signalOwner:Fire()
task.wait()
assert(hits == 3, "Disconnected listeners must not receive a new fire")

local currentConnection
local function closePanel()
    if currentConnection then
        currentConnection:Disconnect()
        currentConnection = nil
    end
end
local function openPanel()
    closePanel()
    currentConnection = signalOwner.Event:Connect(function()
        hits += 1
    end)
end
openPanel()
openPanel()
openPanel()
signalOwner:Fire()
task.wait()
assert(hits == 4, "Reopening must leave only one listener")
closePanel()
closePanel()
signalOwner:Fire()
task.wait()
assert(hits == 4, "Repeated cleanup must be safe and stop new callbacks")
openPanel()
signalOwner:Fire()
task.wait()
assert(hits == 5, "A fresh panel must work after cleanup")
closePanel()

local onceHits = 0
local onceConnection = signalOwner.Event:Once(function()
    onceHits += 1
end)
signalOwner:Fire()
signalOwner:Fire()
task.wait()
assert(onceHits == 1, "Once must handle only the first invocation")
assert(onceConnection.Connected == false)
signalOwner:Destroy()
print("GUIDEBOOK_CONNECTIONS_ENGINE_PASS hits=5 once=1")

检查显式断开 #

第一次触发之后,测试对三条已保存的连接调用Disconnect,并分别检查Connected==false。再执行一次Fire并等待,hits不应变化,仍然是三。因此我们验证的是后续事件不会到达已经断开的监听器。仅把变量改为nil,却忘记断开对象,是另一种操作。

先断开控制器拥有的订阅,再释放保存的引用。不要任意断开其他系统创建和拥有的连接。在真实界面中,在创建位置旁标明负责人,有助于理解清理动作。清理的目标是结束某一个组件的工作,不是全局禁止项目里的所有事件监听器接收信号。

让重复打开成为受管理操作 #

原创openPanel先调用closePanel,然后才创建新订阅,并把返回值保存在currentConnection中。因此连续调用三次openPanel,只留下一个活动监听器。下一次Fire使hits从三增加到四。这是在检查一个新增回调,不是在声称整个实验从头到尾只执行过一个回调。

这种行为适合我们约定的“一个当前面板控制器”。它不是所有界面的通用架构:多个独立面板可能需要不同负责人和独立引用。先规定希望存在多少条活动订阅,再选择辅助函数。随后通过可观察结果检查数量,而不是因为代码短或函数名熟悉,就认定它正确。

重复清理应当安全 #

closePanel检查currentConnection是否存在。如果存在,函数断开订阅,并将字段置为nil。第二次调用不会尝试使用已经释放的引用。测试调用closePanel两次,然后确认新事件不再改变hits。这检查了本控制器的重复结束过程,并不涵盖完整界面的所有生命周期故障。

接下来再次执行openPanel。一次Fire必须使hits增加到五,然后完成最后清理。这一步很重要:如果控制器清理后再也无法创建,仅仅停止旧回调还不够。分别检查两个方向——清理结束旧反应,新创建的控制器按照约定再次只响应一次。把这两个结果写成独立预期。

已观察的响应打开大图 ↗
独立Studio测试结果的原创示意图。

用Once处理第一次调用 #

官方指南建议在回调只需要处理第一次事件时使用Once。测试的独立部分创建onceConnection和新的onceHits计数器,然后触发事件两次。等待后,onceHits应为一,Connected应为false。这个结果来自真实Roblox Studio中的独立BindableEvent,不是用同步替代模型推测的。

第一次信号调用,不一定是业务上第一次合适的情况。如果回调还要检查额外条件,请先决定收到不合适输入后是否继续监听。不要把这个设计问题藏在Once名称后面。一次成功动作与不论参数是什么都只处理第一次信号,可能需要不同方案以及不同测试预期。

尊重事件处理顺序 #

测试在Fire之后执行task.wait,再读取结果。它没有假定回调在下一行代码执行前就已经完成。官方延迟事件文档单独解释队列与恢复点。自制模型中同步增加变量,并不能证明引擎也具有同样行为。因此本材料用一个真实Roblox API实验来支持已经确认的结果。

也不要认为在所有待处理回调场景中,Destroy与Disconnect的效果都相同。官方延迟事件章节区分显式断开和对象销毁时已排队调用的处理。本测试检查断开之后的新事件以及Once,没有覆盖所有队列、已经运行的函数或并行处理。这些情形需要另外的例子和验证,才能作出更广泛结论。

把真实面板检查作为集成阶段 #

后续集成时写下操作序列:创建控制器、重复打开、触发事件、关闭两次、关闭后触发,再重新打开。把UI对象销毁和已经开始的工作作为额外情况。如果反应次数不同,先比较订阅创建次数与预期生命周期,再考虑增加延迟或debounce。时间限制可能隐藏症状,却没有修正连接归属。

频率限制回答一段时间内允许多少次动作;连接归属回答存在多少个监听器以及它们何时结束工作。两种机制不能相互替代。本练习没有网络请求、奖励、保存记录或购买。以后接入这些系统时,需要分别添加结果检查,不能把本地实验的证据扩展为整个游戏已经正常。

阶段预期
三个监听器hits = 3
Disconnect之后hits保持3
重复打开hits = 4
两次清理hits保持4

保存可复现的确认结果 #

修正后的实验在Studio Output中输出GUIDEBOOK_CONNECTIONS_ENGINE_PASS hits=5 once=1。这一行位于关于三个监听器、断开、受管理重复打开、安全清理和Once的assertions之后。教学文件保留精确操作,验证记录保存文件校验值。总数五对应完整实验的连续阶段,而不是单次玩家动作。

向另一位开发者交接时,把源代码、预期输出、调用顺序和验证边界一起保存。不要称它为已完成的面板测试,因为实验里没有真实面板。独立项目没有修改已发布游戏,真实界面的检查仍是下一项任务。准确记录有助于重复错误机制,并在不夸大测试范围的情况下应用所提出的生命周期方案。

边界覆盖范围
新面板hits = 5
Once一个回调
界面GUI单独检查
队列不声称全面覆盖

原始资料

Roblox Creator Hub — Events
Roblox — RBXScriptConnection
Roblox — RBXScriptSignal
Roblox — Deferred engine events