3.2 生命周期与执行顺序
3.1 我们把组件挂上了节点,但组件里的代码什么时候跑、按什么顺序跑还没交代。这一节把 Cocos Creator 3.8 的七个生命周期回调和引擎一帧的调度骨架讲透。卷二里「为什么我的初始化拿到的是 null」「为什么 HUD 数字慢了一帧」这类 bug,根因几乎都在这里。
卷一 6.2 我们用 mini2d 从零实现了同样七个回调,并定义了三条保证(onLoad 早于任何 start、start 早于第一次 update、所有 update 早于任何 lateUpdate)。卷二不再重复推导,重点换成**「引擎给的实际顺序是什么、我怎么当场验证它」**。
3.2.1 七个回调与触发时机
| 回调 | 触发时机 | 调用次数 | 典型用途 |
|---|---|---|---|
onLoad | 组件被激活、进入场景树时 | 一次 | 取兄弟组件引用、建缓存 |
onEnable | 每次从禁用变启用(含首次) | 多次 | 订阅事件、开定时器 |
start | 第一次 update 之前 | 一次 | 依赖「所有 onLoad 已完成」的初始化 |
update | 每帧 | 每帧 | 读输入、算速度 |
lateUpdate | 每帧,所有 update 之后 | 每帧 | 相机跟随、同步显示 |
onDisable | 每次从启用变禁用 | 多次 | 退订事件、关定时器 |
onDestroy | 组件被移除或节点销毁 | 一次 | 释放资源、断开引用 |
在 cc.d.ts 里,这七个都是可选方法(形如 protected onLoad?(): void;)。覆写谁,谁才会被调用;不覆写引擎也不会报错。最小可观察的组件长这样:
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('LifecycleProbe')
export class LifecycleProbe extends Component {
onLoad(): void { console.log('onLoad'); }
onEnable(): void { console.log('onEnable'); }
start(): void { console.log('start'); }
update(dt: number): void { void dt; }
lateUpdate(dt: number): void { void dt; }
onDisable(): void { console.log('onDisable'); }
onDestroy(): void { console.log('onDestroy'); }
}
3.2.2 一帧的调度骨架
把回调接进主循环后,引擎一帧的近似次序是:
// 引擎每帧内部调度(伪代码,帮助理解顺序)
for (const c of activeComponents) c.ensureLoaded(); // onLoad + onEnable(首次)
for (const c of activeComponents) c.startIfNeeded(); // start(首次)
for (const c of activeComponents) c.update(dt); // 全部 update
for (const c of activeComponents) c.lateUpdate(dt); // 全部 lateUpdate
三条不变量记牢就不会踩坑:onLoad 一定早于任何 start;start 一定早于第一次 update;同一帧内所有组件的 update 都早于任何 lateUpdate。 这条「先全跑完 update 再跑 lateUpdate」的规则,是 lateUpdate 能做相机跟随、显示同步的前提——否则相机会比角色慢半帧。
3.2.3 executionOrder 决定同类回调的先后
同一帧里多个组件的 update 谁先谁后?默认按组件挂载顺序,但那是编辑器里的偶然顺序,不可靠。用 @executionOrder(priority) 显式声明,数值小的先执行:
import { _decorator, Component } from 'cc';
const { ccclass, executionOrder } = _decorator;
@ccclass('GameManager')
@executionOrder(-100)
export class GameManager extends Component {
update(dt: number): void { void dt; }
}
@ccclass('Hud')
@executionOrder(100)
export class Hud extends Component {
update(dt: number): void { void dt; }
}
executionOrder 只影响 onLoad、onEnable、start、update、lateUpdate 五个回调,onDisable 和 onDestroy 不受它影响——这条来自 cc.d.ts 里 _decorator.executionOrder 的文档注释。卷二里的约定是:数据层给负数(GameManager 用 -100),玩法层用 0,表现层给正数(Hud 用 100)。这样「先算状态、再更新表现」就由声明保证,而不是靠运气。
3.2.4 帧级事件:director 的四个钩子
如果需要在所有组件之前或之后插一段逻辑,用 director 的帧事件。它们以静态常量暴露在 Director 上:
import { director, Director, Component, _decorator } from 'cc';
const { ccclass } = _decorator;
@ccclass('FrameObserver')
export class FrameObserver extends Component {
onEnable(): void {
director.on(Director.EVENT_BEFORE_UPDATE, this.onBeforeUpdate, this);
director.on(Director.EVENT_AFTER_UPDATE, this.onAfterUpdate, this);
}
onDisable(): void {
director.off(Director.EVENT_BEFORE_UPDATE, this.onBeforeUpdate, this);
director.off(Director.EVENT_AFTER_UPDATE, this.onAfterUpdate, this);
}
private onBeforeUpdate(): void { void director.getDeltaTime(); }
private onAfterUpdate(): void { void director.getTotalFrames(); }
}
四个可用事件是 EVENT_BEFORE_UPDATE / EVENT_AFTER_UPDATE / EVENT_BEFORE_DRAW / EVENT_AFTER_DRAW。EVENT_BEFORE_UPDATE 早于所有组件的 update,EVENT_AFTER_UPDATE 晚于所有 lateUpdate。适合放输入采样、帧计时统计这类「跨组件」逻辑。注意 director 是全局单例,on 一定要和 onDisable 里的 off 成对,否则组件销毁后回调仍持有已释放的 this。
3.2.5 定时调度:schedule 与 scheduleOnce
Component 自带定时器:schedule(callback, interval, repeat, delay) 按固定间隔回调,scheduleOnce(callback, delay) 只回调一次。无限循环用 macro.REPEAT_FOREVER:
import { _decorator, Component, macro } from 'cc';
const { ccclass } = _decorator;
@ccclass('Spawner')
export class Spawner extends Component {
start(): void {
this.schedule(this.spawn, 1.5, macro.REPEAT_FOREVER, 0.5);
this.scheduleOnce(this.firstWave, 0.5);
}
private spawn(): void { /* 生成一组障碍 */ }
private firstWave(): void { /* 第一波提示 */ }
}
schedule 的时间基准是引擎时间,会受 director.pause() 影响,暂停时定时器一起停——这正是我们想要的(暂停菜单弹出时障碍不该继续生成)。它和 update 里手写累加计时是等价的,但 schedule 由引擎统一管理,组件销毁时自动清理;手写累加则必须在 onDisable 里记得停。
3.2.6 启用与禁用:一对必须配对的回调
onEnable 和 onDisable 是七回调里唯一「成对多次触发」的一组。规则很简单:在 onEnable 里做的事,必须在 onDisable 里撤销。最常见的是事件订阅:
import { _decorator, Component, EventTarget } from 'cc';
const { ccclass } = _decorator;
const bus = new EventTarget();
@ccclass('CoinCounter')
export class CoinCounter extends Component {
private count = 0;
onEnable(): void {
bus.on('coin:collected', this.onCoin, this);
}
onDisable(): void {
bus.off('coin:collected', this.onCoin, this);
}
private onCoin(): void {
this.count += 1;
}
}
不配对的后果是「一次事件触发多次结算」:组件被反复启停几轮后,同一个回调在 bus 里注册了多份,金币加分会翻倍。Cocos 的 EventTarget.on 接受第三个 target 参数用于绑定 this,但它不返回退订函数(这点和卷一 6.2 我们自己写的 EventBus 不同),所以退订必须自己记住 type 和回调引用。
| 回调 | 该做的事 | 不该做的事 |
|---|---|---|
onLoad | 取引用、建缓存 | 依赖其他组件 start 后的状态 |
onEnable | 订阅、开定时器、播动画 | 做重计算(会随启停反复执行) |
start | 依赖已就绪的初始化 | 每帧都需要的逻辑 |
update | 读输入、算速度 | 分配临时对象、console.log |
lateUpdate | 相机、显示同步 | 修改 update 阶段已定稿的状态 |
onDisable | 退订、关定时器 | 释放还需复用的资源 |
onDestroy | 释放资源、断引用 | 访问已销毁的兄弟节点 |
3.2.7 首帧陷阱与节点激活
三个高频坑,卷二全程规避:
start在节点未激活时不触发。 如果一个节点在场景里默认active = false,它的组件只跑onLoad,start要等到第一次被激活。把「初始化玩家」放在start里,而玩家节点默认隐藏,就会看到「游戏卡在准备界面」。onLoad里拿不到别的组件已经start完的状态。onLoad阶段所有组件都还没start,此时读other.startTime拿到的是默认值。要跨组件读「已初始化」的字段,放到start里,或用executionOrder保证顺序。addComponent是同步生效的。 运行时node.addComponent(X)之后,X的onLoad会立即被调用(若节点已激活),但start要等到下一帧的start阶段。所以在addComponent之后立刻读它的start初始化结果,也是null。onLoad里销毁自己会漏掉start。 在onLoad中调用this.destroy(),引擎会把该组件标记为待销毁,start与后续update都不再触发;但同一帧里其他组件的onLoad仍会执行完。
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('SelfDestruct')
export class SelfDestruct extends Component {
onLoad(): void {
if (this.node.name === 'Temp') {
this.destroy(); // 本组件的 start / update 不再触发
}
}
}
这类「自己把自己干掉」的写法只在对象池回收临时节点时才合理,普通业务组件应当避免。
3.2.8 在《方块冲刺》里验证顺序
把探针组件挂到 main.scene 的 GameRoot 上,再挂上 GameManager(-100)、PlayerController(0)、Hud(100),运行时控制台应看到:
GameManager onLoad
PlayerController onLoad
Hud onLoad
GameManager start
PlayerController start
Hud start
... 每帧 GameManager update / PlayerController update / Hud update ...
三组 onLoad 全部早于任何 start,且 update 严格按 -100 → 0 → 100 的顺序,就是 3.2.3 的约定在生效。若 Hud 的数字比 PlayerController 的位置早更新一帧,说明 Hud 的 executionOrder 设反了。
3.2.9 编辑器里的验证步骤(需编辑器,不可命令行复现)
本节涉及 .scene 工程文件,需 Cocos Creator 3.8 编辑器打开,本节不可命令行复现。 步骤:
- 打开
assets/scenes/main.scene,层级管理器里选中GameRoot节点。 - 属性检查器「添加组件」→ 依次搜索并添加
GameManager、PlayerController、Hud。 - 运行预览(编辑器右上角三角按钮),打开浏览器控制台。
- 控制台应输出上面那串顺序。
打开 main.scene 会看到 GameRoot 节点的 _components 数组里有三项,__type__ 分别是注册名:
{
"__type__": "cc.Node",
"_name": "GameRoot",
"_components": [
{ "__type__": "GameManager" },
{ "__type__": "PlayerController" },
{ "__type__": "Hud" }
]
}
注意 .scene 里看不到 executionOrder——它是类级装饰器,编译进脚本而非存进场景,所以改 @executionOrder 后要重新构建才生效,热重载不一定立即反映。
下一节我们让组件之间能互相说话:节点事件、全局事件总线,以及触摸与键盘输入怎么接进来。
阅读导航:上一节:3.1 @ccclass / @property 与类型系统 · 下一节:3.3 组件通信与事件 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。