外观
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 后只要等一帧就一定没有引用
外部事件、闭包、缓存和对象池仍可能保留引用,需要主动设计清理。
十二、练习与答案
练习
- 组件从创建到 update 需要经过哪些关键状态?
- 为什么
onEnable适合注册事件? - 节点从 inactive 变 active 时,应该把初始化写在 start 还是 onEnable?
- 为什么销毁对象后还要检查异步回调?
- 组件 enabled=false 和节点 active=false 的调度范围有什么区别?
参考答案
- 创建、挂载、进入场景、满足激活条件、onLoad、onEnable、start、update。
- 因为事件只在组件可用期间有效,onDisable 可以对称取消。
- 每次打开都需要执行的逻辑放在 onEnable 或显式打开方法,首次初始化才放 start。
- 异步回调可能在对象已经失效后执行,继续访问会产生错误或泄漏。
- 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下一课会进一步讨论多个组件同时更新时,先后顺序如何影响业务结果。
