Creator | 脚本组件的生与死-生命周期函数回调触发时机

547 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

官方文档:

生命周期回调:

docs.cocos.com/creator/man…

​测试脚本

const { ccclass, property } = cc._decorator;​@ccclassexport default class LifeCycle extends cc.Component {​    // LIFE-CYCLE CALLBACKS:       __preload() {        debugger    }​    onLoad() {        debugger    }​    onEnable() {        debugger    }​    start() {        debugger    }​    update(dt) {        // debugger    }​    lateUpdate(dt) {        // debugger    }​    onDisable() {        debugger    }​    onDestroy() {        debugger    }​    clickEvent() {        // this.node.active = false;        // this.node.active = true;        // this.node.destroy();        this.destroy();    }}

触发顺序

一个组件从初始化到激活,再到销毁的生命周期函数调用顺序为:

接下来我们按生命周期触发的先后顺序看一下,每个回调函数的调用时机

要特别区分一下,生命周期是依赖 节点 还是依赖 组件

__preload、onLoad、onDestroy 依赖 节点 

onEnable、start、update、lateUpdate、onDisable 依赖 组件

它们的区别在于:

如果脚本组件所在节点的 active = true,但脚本组件的 enabled = false,依然会执行 节点 相关回调,但不会执行 组件 相关回调,只有在 enabled = true 的情况下,组件 相关回调才会被执行

如果脚本组件所在节点的 active = false,无论脚本组件的 enabled 为 true 或 false,都不会执行任何回调

调用时机

1 __preload onLoad onEnable

·  __preload

官方文档没有说明,以下仅是个人理解

见名知意,其功能和 onLoad 相同,仅仅是调用时机在 onLoad 之前

· onLoad

脚本组件的初始化阶段,该回调会在节点首次激活时触发,比如所在的场景被载入,或者所在节点被激活的情况下

在 onLoad 阶段,保证了你可以获取到场景中的其他节点,以及节点关联的资源数据

该回调总是会在 start 方法调用前执行,这能用于安排脚本的初始化顺序

· onEnable

当组件的 enabled 属性从 false 变为 true 时,或者所在节点的 active 属性从 false 变为 true 时,会激活 onEnable 回调

倘若节点第一次被创建且 enabled 为 true,则会在 onLoad 之后,start 之前被调用

调用时机:

从以上调用时机可以推断,如果一个节点挂载了多个脚本组件,那么执行顺序是:

所有脚本组件的 onLoad → 所有脚本组件的 onEnable → 所有脚本组件的 start  等等

· 当节点激活后,即 active = true:

相关源码路径:

resources\engine\cocos2d\core\node-activator.js

resources\engine\cocos2d\core\component-scheduler.js

① active:访问器属性

当前节点的自身激活状态

值得注意的是,一个节点的父节点如果不被激活,那么即使它自身设为激活,它仍然无法激活

active: {    get () {        return this._active;    },    set (value) {        value = !!value;        if (this._active !== value) {            this._active = value;            var parent = this._parent;            if (parent) {                // 父节点的激活状态                var couldActiveInScene = parent._activeInHierarchy;                if (couldActiveInScene) {                    // 更改节点激活状态                    cc.director._nodeActivator.activateNode(this, value);                }            }        }    }}

② activateNode:更改节点激活状态

activateNode (node, active) {    // 激活    if (active) {        var task = activateTasksPool.get();        this._activatingStack.push(task);​        // 递归激活所有子节点及节点上的所有组件        this._activateNodeRecursively(node, task.preload, task.onLoad, task.onEnable);        // 调用 __preload()        task.preload.invoke();        // 调用 onLoad()        task.onLoad.invoke();        // 调用 onEnable()        task.onEnable.invoke();​        this._activatingStack.pop();        activateTasksPool.put(task);    }     // 禁用    else {        // 递归禁用所有子节点及节点上的所有组件        this._deactivateNodeRecursively(node);​        // remove children of this node from previous activating tasks to debounce        // (this is an inefficient operation but it ensures general case could be implemented in a efficient way)        var stack = this._activatingStack;        for (var i = 0; i < stack.length; i++) {            var lastTask = stack[i];            lastTask.preload.cancelInactive(IsPreloadStarted);            lastTask.onLoad.cancelInactive(IsOnLoadStarted);            lastTask.onEnable.cancelInactive();        }    }    node.emit('active-in-hierarchy-changed', node);}

其中 task.preload,task.onLoad,task.onEnable 

分别对应 _activateNodeRecursively 中的  preloadInvoker,onLoadInvoker,onEnableInvoker

另外关于生命周期中的几个调用者方法,由于篇幅所限,就不再啰嗦了,感兴趣的可以自己查阅

UnsortedInvoker

OneOffInvoker

ReusableInvoker

invokePreload

invokeOnLoad

invokeOnEnable

invokeStart

invokeUpdate

invokeLateUpdate

③ _activateNodeRecursively:递归激活所有子节点及节点上的所有组件

_activateNodeRecursively (node, preloadInvoker, onLoadInvoker, onEnableInvoker) {    // 如果节点正在反激活的过程中则返回    if (node._objFlags & Deactivating) {        // 对相同节点而言,无法撤销反激活,防止反激活 - 激活 - 反激活的死循环发生。        // 这样设计简化了一些引擎的实现,而且对调用者来说能保证反激活操作都能成功。        cc.errorID(3816, node.name);        return;    }​    node._activeInHierarchy = true;​    // component maybe added during onEnable, and the onEnable of new component is already called    // so we should record the origin length    var originCount = node._components.length;    // 激活该节点所有组件    for (let i = 0; i < originCount; ++i) {        let component = node._components[i];        if (component instanceof cc.Component) {            this.activateComp(component, preloadInvoker, onLoadInvoker, onEnableInvoker);        } else {            _componentCorrupted(node, component, i);            --i;            --originCount;        }    }​    node._childArrivalOrder = node._children.length;        // activate children recursively    for (let i = 0, len = node._children.length; i < len; ++i) {        let child = node._children[i];        // 根据子节点排列顺序设置 _localZOrder        child._localZOrder = (child._localZOrder & 0xffff0000) | (i + 1);        if (child._active) {            // 递归调用            this._activateNodeRecursively(child, preloadInvoker, onLoadInvoker, onEnableInvoker);        }    }    node._onPostActivated(true);}

_localZOrder 和 zIndex 的关系为:

zIndex = _localZOrder >> 16_localZOrder = (_localZOrder & 0x0000ffff) | (value << 16)

④ activateComp:激活组件,填充对应的生命周期回调

activateComp: function (comp, preloadInvoker, onLoadInvoker, onEnableInvoker) {    if (!cc.isValid(comp, true)) {        // destroyed before activating        return;    }    if (!(comp._objFlags & IsPreloadStarted)) {        comp._objFlags |= IsPreloadStarted;        if (comp.__preload) {            if (preloadInvoker) {                preloadInvoker.add(comp);            } else {                comp.__preload();            }        }    }    if (!(comp._objFlags & IsOnLoadStarted)) {        comp._objFlags |= IsOnLoadStarted;        if (comp.onLoad) {            if (onLoadInvoker) {                onLoadInvoker.add(comp);            } else {                comp.onLoad();                comp._objFlags |= IsOnLoadCalled;            }        } else {            comp._objFlags |= IsOnLoadCalled;        }    }    if (comp._enabled) {        var deactivatedOnLoading = !comp.node._activeInHierarchy;        if (deactivatedOnLoading) {            return;        }        // 启用组件        cc.director._compScheduler.enableComp(comp, onEnableInvoker);    }}

· 当组件启用后,即 enable = true:

相关源码路径:

resources\engine\cocos2d\core\components\CCComponent.js

① enabled:访问器属性

表示该组件自身是否启用

enabled: {    get () {        return this._enabled;    },    set (value) {        if (this._enabled !== value) {            this._enabled = value;            if (this.node._activeInHierarchy) {                var compScheduler = cc.director._compScheduler;                if (value) {                    // 启动该组件                    compScheduler.enableComp(this);                } else {                    // 禁用该组件                    compScheduler.disableComp(this);                }            }        }    },    visible: false,    animatable: true}

② enableComp:启用组件,填充对应的生命周期回调

enableComp: function (comp, invoker) {    if (!(comp._objFlags & IsOnEnableCalled)) {        if (comp.onEnable) {            if (invoker) {                invoker.add(comp);                return;            } else {                comp.onEnable();​                var deactivatedDuringOnEnable = !comp.node._activeInHierarchy;                if (deactivatedDuringOnEnable) {                    return;                }            }        }        this._onEnabled(comp);    }}

③ _onEnabled:启用组件

_onEnabled (comp) {    cc.director.getScheduler().resumeTarget(comp);    comp._objFlags |= IsOnEnableCalled;​    // schedule    if (this._updating) {        // 如果在当前帧内激活该组件,则会被放入到延时队列中        this._deferredComps.push(comp);    } else {        this._scheduleImmediate(comp);    }

如果是在当前帧内激活组件,就会把该组件放入到延时队列中,等到当前帧结束时(lateUpdatePhase)触发 start 回调,然后在下一帧触发 update 和 lateUpdate

lateUpdatePhase:

lateUpdatePhase (dt) {    this.lateUpdateInvoker.invoke(dt);​    // End of this frame    this._updating = false;​    this._startForNewComps();}

其中 __preload 、onLoad 通过对标志位(_objFlags)进行位运算来判断是否填充过对应的回调,以保证生命周期内只调用一次

if (!(comp._objFlags & IsPreloadStarted)) {    comp._objFlags |= IsPreloadStarted;    xxx}if (!(comp._objFlags & IsOnLoadStarted)) {    comp._objFlags |= IsOnLoadStarted;    xxx}

而 onEnable 是通过判断该组件是否启用(_enabled),所以该组件每次启用后都会被调用一次

if (comp._enabled) {    xxx}

· 利用位运算设置标志位

① 设置标志位

comp._objFlags |= IsOnEnableCalled;

② 关闭标志位

comp._objFlags &= ~IsOnEnableCalled;

③ 判断标志位

if (comp._objFlags & IsOnEnableCalled)

3start update lateUpdate

· start

start 回调函数会在组件第一次激活后,也就是第一次执行 update 之前触发

· update

游戏开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 update 回调中

· lateUpdate

update 会在所有动画更新前执行,但如果我们要在动效(如动画、粒子、物理等)更新之后才进行一些额外操作,或者希望在所有组件的 update 都执行完之后才进行其它操作,那就需要用到 lateUpdate 回调

调用时机:

相关源码路径:

resources/engine/cocos2d/core/CCDirector.js

mainLoop:

Run main loop of director

mainLoop: function (now) {    if (this._purgeDirectorInNextLoop) {        this._purgeDirectorInNextLoop = false;        this.purgeDirector();    } else {        // calculate "global" dt        this.calculateDeltaTime(now);​        // Update        if (!this._paused) {            // before update            this.emit(cc.Director.EVENT_BEFORE_UPDATE);​            // Call start for new added components            this._compScheduler.startPhase();​            // Update for components            this._compScheduler.updatePhase(this._deltaTime);            // Engine update with scheduler            this._scheduler.update(this._deltaTime);​            // Late update for components            this._compScheduler.lateUpdatePhase(this._deltaTime);​            // User can use this event to do things after update            this.emit(cc.Director.EVENT_AFTER_UPDATE);                        // Destroy entities that have been removed recently            Obj._deferredDestroy();        }​        // Render        this.emit(cc.Director.EVENT_BEFORE_DRAW);        renderer.render(this._scene, this._deltaTime);​        // After draw        this.emit(cc.Director.EVENT_AFTER_DRAW);​        eventManager.frameUpdateListeners();        this._totalFrames++;    }}

4onDisable onDestroy

· onDisable

当组件的 enabled 属性从 true 变为 false 时,或者所在节点的 active 属性从 true 变为 false 时,会激活 onDisable 回调

· onDestroy

当组件或者所在节点调用了 destroy(),则会调用 onDestroy 回调,并在当前帧结束时统一回收组件

· 当节点禁用后,即 active = false:

相关源码路径:

resources\engine\cocos2d\core\node-activator.js

① _deactivateNodeRecursively:递归禁用所有子节点及节点上的所有组件

_deactivateNodeRecursively (node) {    // 设置标志位    node._objFlags |= Deactivating;    node._activeInHierarchy = false;        // 禁用该节点所有组件    var originCount = node._components.length;    for (let c = 0; c < originCount; ++c) {        let component = node._components[c];        if (component._enabled) {            cc.director._compScheduler.disableComp(component);​            if (node._activeInHierarchy) {                // reactivated from root                node._objFlags &= ~Deactivating;                return;            }        }    }        //递归调用    for (let i = 0, len = node._children.length; i < len; ++i) {        let child = node._children[i];        if (child._activeInHierarchy) {            this._deactivateNodeRecursively(child);​            if (node._activeInHierarchy) {                // reactivated from root                node._objFlags &= ~Deactivating;                return;            }        }    }​    node._onPostActivated(false);    node._objFlags &= ~Deactivating;}

· 当组件禁用后,即 enable = false:

相关源码路径:

resources\engine\cocos2d\core\component-scheduler.js

① disableComp:禁用组件

disableComp: : function (comp) {    if (comp._objFlags & IsOnEnableCalled) {        if (comp.onDisable) {            comp.onDisable();        }        this._onDisabled(comp);    }}

· 当节点销毁后,即 node.destroy():

相关源码路径:

resources\engine\cocos2d\core\utils\base-node.js

destroy:销毁节点

destroy () {    if (cc.Object.prototype.destroy.call(this)) {        this.active = false;    }}

① cc.Object.prototype.destroy.call(this):

销毁该对象,并释放所有它对其它对象的引用

实际销毁操作会延迟到当前帧渲染前执行,从下一帧开始,该对象将不再可用

您可以在访问对象之前使用 cc.isValid(obj) 来检查对象是否已被销毁

相关源码路径:

resources\engine\cocos2d\core\platform\CCObject.js

destroy:

prototype.destroy = function () {    if (this._objFlags & Destroyed) {        cc.warnID(5000);        return false;    }    if (this._objFlags & ToDestroy) {        return false;    }    // 设置标志位    this._objFlags |= ToDestroy;    // 放入待销毁队列,在当帧结束时统一回收组件    objectsToDestroy.push(this);​    return true;};

② this.active = false

同禁用节点

· 当组件销毁后,即 comp.destroy()

destroy:组件销毁

destroy () {    if (CC_EDITOR) {        var depend = this.node._getDependComponent(this);        if (depend) {            return cc.errorID(3626,                cc.js.getClassName(this), cc.js.getClassName(depend));        }    }    if (this._super()) {        if (this._enabled && this.node._activeInHierarchy) {            cc.director._compScheduler.disableComp(this);        }    }}

① disableComp

同禁用组件

· onDestroy() 的触发时机

当一个对象的 destroy 调用以后,会在当前帧结束后才真正销毁

注意:是当前帧结束后销毁,而不是下一帧销毁

相关源码路径:

resources\engine\cocos2d\core\platform\CCObject.js

deferredDestroy:销毁所有待销毁队列中的数据

function deferredDestroy () {    var deleteCount = objectsToDestroy.length;    for (var i = 0; i < deleteCount; ++i) {        var obj = objectsToDestroy[i];        if (!(obj._objFlags & Destroyed)) {            // 立即销毁            obj._destroyImmediate();        }    }    // if we called b.destory() in a.onDestroy(), objectsToDestroy will be resized,    // but we only destroy the objects which called destory in this frame.    if (deleteCount === objectsToDestroy.length) {        objectsToDestroy.length = 0;    } else {        objectsToDestroy.splice(0, deleteCount);    }​    if (CC_EDITOR) {        deferredDestroyTimer = null;    }}

 

更多教程请扫码关注