Skip to content

Stage02|Engine Loop ​

Lesson016|组件生命周期如何注册、启用和移出调度队列? ​

Tags: #Creator2.x #Stage02 #Component #Lifecycle #SchedulerDifficulty: ⭐⭐⭐⭐☆


一、本课目标 ​

上一课介绍了调度器,本课进一步追踪一个组件的运行状态:

text
组件被创建
→ 什么时候进入调度系统
→ 什么时候执行 onLoad / onEnable / start
→ 什么时候执行 update
→ 什么时候被移出调度

本课重点是生命周期和调度的关系,不把某个内部数组名称当成公共 API。


二、组件从创建到运行 ​

可以先用这张状态图理解:

text
组件实例创建
    ↓
挂到 Node
    ↓
完成反序列化或运行时初始化
    ↓
Node 进入场景树
    ↓
满足激活条件
    ↓
onLoad
    ↓
onEnable
    ↓
start
    ↓
update / lateUpdate

这里的“进入调度系统”不是简单等于“构造函数执行”。组件可能已经存在,但节点尚未进入场景或尚未激活,因此还不能正常执行每帧逻辑。


三、onLoad 为什么不等于 start ​

onLoad:完成自身基础准备 ​

ts
onLoad() {
    this.elapsed = 0;
    this.cache = this.getComponent(cc.Label);
}

适合:

  • 初始化自身字段。
  • 获取同节点组件。
  • 读取已经序列化的配置。
  • 建立不依赖“所有对象都已开始运行”的基础状态。

start:进入第一次正式更新前的准备 ​

ts
start() {
    this.refreshFromGameManager();
}

它更适合依赖其他组件或场景整体已经完成初步初始化的逻辑。

简单记忆:

text
onLoad:我被创建并准备自身。
onEnable:我当前处于可用状态。
start:我准备进入第一次正式更新。

四、组件为什么需要注册 ​

引擎不应该每帧对每个 Component 都猜一次:

text
这个组件有没有 update?
enabled 吗?
节点激活吗?
已经销毁了吗?

因此组件状态变化时,调度系统通常会进行登记或更新:

text
组件启用
    ↓
加入对应生命周期阶段

组件禁用
    ↓
从可执行状态移出或标记不可执行

节点父级关闭
    ↓
activeInHierarchy 变化
    ↓
子树组件状态重新评估

这是“生命周期回调”和“调度队列”连接起来的地方。


五、active、enabled 和调度的关系 ​

text
Node.active
    ↓ 与父节点共同决定
Node.activeInHierarchy
    ↓
组件是否处于可用场景树

Component.enabled
    ↓
组件自身是否允许工作

一个组件想执行 update,通常需要:

text
enabled = true
activeInHierarchy = true
存在 update 方法
未销毁
主循环正在运行

只关闭组件 ​

ts
this.move.enabled = false;

只影响 move 组件;同一 Node 上的 Sprite、Button 或其他脚本不一定停止。

关闭节点 ​

ts
this.node.active = false;

会影响节点树的最终激活状态,子节点组件也会受到影响。


六、onEnable 和 onDisable 的正确用途 ​

事件绑定通常应该和可用状态绑定:

ts
onEnable() {
    EventBus.on('player-change', this.onPlayerChange, this);
}

onDisable() {
    EventBus.off('player-change', this.onPlayerChange, this);
}

这样节点关闭时不会继续收到业务事件。

如果只在 onLoad 绑定,却在节点反复 active 时没有取消,可能出现:

  • 隐藏界面仍收到事件。
  • 同一个回调重复执行。
  • 旧场景对象被全局事件总线持有。
  • 场景切换后内存无法回收。

七、同一帧中启用组件会怎样 ​

ts
this.enemy.enabled = true;

这通常意味着组件从不可用状态进入可用状态,但不应简单推断“这一行后马上执行完整的 onLoad、start、update”。实际执行时机会受:

  • 组件是否首次加载。
  • 节点是否 activeInHierarchy。
  • 当前处于哪个调度阶段。
  • 当前 Creator 版本的调度实现。

稳定的工程原则是:

启用组件后,不要依赖它在同一段同步代码中已经完成所有生命周期。

如果业务需要明确的初始化完成点,应提供显式方法或状态,而不是依赖隐含的回调顺序。


八、start 为什么通常只执行一次 ​

start 通常表示第一次进入正式运行前的初始化阶段:

text
第一次满足运行条件
    ↓
执行 start
    ↓
标记 start 已完成
    ↓
后续只执行 update / lateUpdate

节点从 active=false 变为 true 时,通常会再次触发 onEnable,但不应把它等同于一定再次触发 start。

如果某段逻辑需要每次打开界面都执行,应放在明确的打开方法或 onEnable,而不是赌 start 会再次运行。


九、销毁和调度移除 ​

ts
this.node.destroy();

从运行时角度至少有三个阶段:

text
标记销毁
    ↓
停止或不再加入后续调度
    ↓
执行清理生命周期
    ↓
释放对象和相关引用

业务代码不应该依赖销毁对象继续执行后续逻辑。尤其要注意:

  • 延迟回调仍可能持有旧对象引用。
  • 事件总线仍可能尝试通知旧对象。
  • 对象池回收对象时不一定真的销毁。

销毁、禁用和回收是三种不同策略。


十、案例:可反复打开的弹窗 ​

ts
onEnable() {
    this.refreshData();
    this.eventBus.on('reward-change', this.refreshData, this);
}

onDisable() {
    this.eventBus.off('reward-change', this.refreshData, this);
}

推荐这样设计:

text
onLoad:获取引用、初始化固定字段
onEnable:刷新当前显示、注册当前需要的事件
onDisable:取消事件、停止本界面任务
start:只做首次运行初始化

不要在每次打开时重新 instantiate 一个永久不销毁的弹窗,也不要把所有事件都绑定到全局生命周期。


十一、常见误区 ​

误区一:构造完成就可以 update ​

组件还要进入场景、满足激活条件并被调度系统接纳。

误区二:onLoad 每次 active=true 都执行 ​

通常不会。它主要对应首次加载和初始化。

误区三:onEnable 里注册事件,onLoad 里取消 ​

如果节点反复启用,这会导致事件状态不匹配。通常在 onEnable 注册、onDisable 取消。

误区四:enabled=false 会隐藏 Node ​

它主要关闭一个组件,不等于关闭节点渲染。

误区五:destroy 后只要等一帧就一定没有引用 ​

外部事件、闭包、缓存和对象池仍可能保留引用,需要主动设计清理。


十二、练习与答案 ​

练习 ​

  1. 组件从创建到 update 需要经过哪些关键状态?
  2. 为什么 onEnable 适合注册事件?
  3. 节点从 inactive 变 active 时,应该把初始化写在 start 还是 onEnable?
  4. 为什么销毁对象后还要检查异步回调?
  5. 组件 enabled=false 和节点 active=false 的调度范围有什么区别?

参考答案 ​

  1. 创建、挂载、进入场景、满足激活条件、onLoad、onEnable、start、update。
  2. 因为事件只在组件可用期间有效,onDisable 可以对称取消。
  3. 每次打开都需要执行的逻辑放在 onEnable 或显式打开方法,首次初始化才放 start。
  4. 异步回调可能在对象已经失效后执行,继续访问会产生错误或泄漏。
  5. enabled 只影响单个组件,active 会影响节点及其子树的最终激活状态。

十三、Creator 2.4.x 可复现实验:生命周期探针 ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class LifecycleProbe extends cc.Component {
    @property(cc.Label) label: cc.Label = null;
    onLoad() { cc.log('[life] onLoad'); }
    onEnable() { cc.log('[life] onEnable'); }
    start() { cc.log('[life] start'); }
    update() { cc.log('[life] update'); this.enabled = false; }
    onDisable() { cc.log('[life] onDisable'); }
    onDestroy() { cc.log('[life] onDestroy'); }
}

运行后在编辑器或按钮中反复切换节点 active 与组件 enabled,再销毁节点。预期首次进入激活状态才有 onLoad/start,每次从禁用到启用会出现成对的 onEnable,关闭组件或节点会触发 onDisable。不要把“脚本对象已构造”当作“已经加入 update 队列”。

失败诊断 ​

现象检查
没有任何日志脚本编译、组件挂载、节点是否在当前 Scene
start 多次执行的误判先确认是同一实例还是对象池重新创建
回收后仍收到事件监听对象是否全局、onDisable 是否取消

十四、版本边界与设计决策 ​

本课以 Creator 2.4.x 生命周期回调为准。onLoad、onEnable、start、update、onDisable、onDestroy 的语义稳定,但动态激活时某些回调与调度队列的同帧交界属于实现细节。对象池场景应把“首次初始化”和“每次取出重置”分开,不能用 start 代替重置逻辑。

十五、应用题与推导答案 ​

题目: 页面通过网络请求加载数据,用户快速关闭页面后请求返回,怎样避免异常?

答案: 在组件中保存请求序号或取消句柄;onDisable 使 session 失效;回调回到脚本线程后先判断 this.isValid、组件 enabled 和 session 是否仍匹配,再写 Label。这样即使网络层不能取消底层请求,也不会把结果写入已离场视图。

十六、本课总结 ​

text
组件实例不等于可执行组件。
进入场景和激活状态决定它能否加入正常调度。
onLoad、onEnable、start、update 解决不同阶段的问题。
onEnable 注册的事件优先在 onDisable 取消。
禁用、销毁、对象池回收必须分清生命周期语义。

十七、下一课预告 ​

text
Lesson017|update、lateUpdate 与 executionOrder

下一课会进一步讨论多个组件同时更新时,先后顺序如何影响业务结果。