脚本热重载:清除模块缓存不能撤销世界副作用

自动关联目录:脚本热重载:清除模块缓存不能撤销世界副作用

核心判断:package.loaded[name] = nil 只保证下一次 require 会重跑模块,不会自动断开旧 Delegate、Timer、网络 handler,也不会迁移旧实例所持有的函数和状态。

一、实际问题:长生命周期 VM 遇到重复试玩

为减少初始化成本,VM 可以跨多次编辑预览长期存在。第一次加载后,用户模块进入 package.loaded;第二次试玩继续命中缓存,顶层注册代码不再执行,看起来就像脚本没有启动。

现有实现会在框架加载完成后抓取 baseline,在新一轮运行前清除 baseline 之外的用户模块,并使构造缓存失效。收集 key 与真正删除分成两遍,避免迭代 package.loaded 时修改同一张表。这是针对模块缓存问题的准确修复,不应否定。

单个自定义类型也支持重扫定义、覆盖注册表并重新校验元数据;删除路径会从多个注册表注销,但明确不销毁已有实例。这恰好揭示了热重载的真实边界。

二、结构性缺陷:代码版本和活对象版本会分叉

旧对象可能仍保存旧闭包;旧模块注册的事件、协程和网络 slot 仍然活着;新定义修改字段 Schema 后,旧实例状态也不会自动升级。若只看 require 是否重新执行,重复绑定和新旧逻辑并存会比“不重载”更难诊断。

因此要区分三种动作:

动作解决的问题不保证什么
模块缓存失效新 require 读取新代码旧副作用撤销
类型注册表替换新实例使用新定义旧实例迁移
运行域重启重新建立对象与订阅保留调试状态

三、目标设计:热重载必须拥有 Generation 和资源清单

domain = {
    generation = generation + 1,
    connections = {}, timers = {}, tasks = {}, remotes = {}, instances = {}
}

function reload(change)
    local candidate = compileAndValidate(change)
    local migration = candidate.planMigration(snapshotState(domain))
    domain:quiesce()
    domain:cancelAll()
    if not migration:apply() then
        domain:restorePreviousGeneration()
        return false
    end
    domain:activate(candidate)
    return true
end

所有可逃逸副作用都通过运行域创建并登记;异步完成携带 generation,旧代完成只能释放自己的资源。无法证明可迁移的类型变化直接重启该运行域,而不是勉强保留活对象。

四、验证

测试连续重载同一模块、重命名类型、删除类型、字段增删、运行中协程、已绑定网络事件和资源异步完成。除了验证新代码生效,还要验证订阅数不增长、旧代回调不执行、失败迁移能恢复。

复盘结论是:模块重载是装载机制,热重载则是一笔带迁移和补偿的运行时事务。