V7投稿 | 带你全面、系统的了解周期函数(含源码分析)

简介

​在论坛里搜了一下,没找到一篇写周期函数的详细文章,赶在征文活动的热度,整理了一下,发出来,大家一起学习学习。基于 cocosCreator2.4.12

文章将会由浅入深的、系统的分析一下cocos的生命周期函数。主要分为以下阶段,第一阶段:简单的知道cocos周期函数有哪些,以及常规的执行顺序。第二阶段:与其他情形结合,去理解cocos周期函数。第三阶段:cocos 周期函数的实现,触发时机,分析代码实现。第四阶段 进行简单的总结一下。

什么是周期函数?如果你在脚本组件中定义了周期函数,那么cocos引擎会在其特定的时期自动执行相关的周期函数。

第一 初步了解周期函数

初学cocos的同学,大部分应该都是从cocos文档中了解、学习周期函数的。这里先贴上官方文档地址:Cocos Creator 2.4 手册 - 生命周期回调。这里我给简单总结归纳一下,也可省了大家时间。默认节点已激活,脚本组件已启用

  1. cocos周期函数有:(__preload)、onLoad、onEnabel、start、update、lateUpdate、onDisable、onDestroy。
  2. 执行顺序:onLoad、onEnable、start、update、lateUpdate

image

  1. 节点显示隐藏:onEnable、onDisable

image

  1. 节点删除:onDisable、onDestroy

image

第二 进一步认识周期函数

​在第一阶段 我们对周期函数有了一个初步认识了,但那是基于单个节点的认知。现在我们再进一步的了解周期函数。

一、结合节点树来了解一下周期函数

cocos 的节点结构本质上是一个特殊的树形结构,一个节点可以多个子节点,但是一个子节点只有一个唯一的父节点。

那在节点树中,周期函数的执行顺序是什么样的呢? 默认节点已激活,脚本组件已启用

测试的节点结构如下:

image

周期函数执行顺序

image

根据打印信息我们可以看到,执行顺序是从上往下依次执行,每个周期函数都是全部执行完后再执行下一个周期函数。

执行顺序是:所有的onLoad => 所有的onEnable => 所有的start => 所有的update => 所有的lateUpdate

二、节点激活、脚本组件启用

细心的同学应该会发现,前面都有一个强调节点已激活,脚本组件已启用。所以这里我们说一下节点激活、脚本组件启用与周期函数的关系。
image

注意:loudspeaker:

1,当节点在节点树中,且处于激活状态,节点上的脚本组件不管有没有启用,都会执行__preoload、onLoad 函数。

2,脚本组件的启用还控制 start的调用,即首次启用 会执行start

三、node.parent、setParent、addChild 与 周期函数

在我们给节点设置父节点后,会立即执行节点上脚本组件的周期函数:__prelaod、onLoad、onEnable

image

image

image

四、addComponent 与周期函数

addComponent 同 三 的触发顺序。

在 addComponent 时,即会立刻触发 __preload、onLoad、onEnable。start、update、lateUpdate 按照正常顺序触发。因为会立刻调用 activateComp 函数。然后再按深度遍历顺序触发周期函数。

  1. 在onload、start 中addComponent 会立即触发__preload、onLoad、onEnable。
  2. 在update、lateUpdate中 addComponent 会在lateUpdate之后再,触发__preload、onLoad、onEnable。

五、exectionOrder 与 周期函数

@exectionOrder(number)

在脚本组件中,我们可以通过 exectionOrder 来控制脚本组件的执行顺序。该设置是 相对于整个节点树的所有脚本组件而言的,不是单个节点的所有脚本组件。

例如:脚本组件的 exectionOrder 默认值均是 0,如果任意的一个节点上的一个组件的 exectionOrder 设置为-1,则该组件的生命周期函数会在所有的的脚本组件函数之前执行(是相对于单独的某个周期函数的执行而言),即onLoad 会在所有脚本组件的 onLoad 之前执行,start 会在所有脚本组件的 start 之前执行等。

第三 从源码中 学习周期函数

这里我们来从代码层面分析周期函数,主要是 component-scheduler、node-activator 两个核心类。

  • node-activator 类主要负责节点的__preload、onLoad、onEnable三个周期函数的触发功能。其次还有onDestroy函数的直接调用功能。该类主要函数有 activateNode、activateComp、destroyComp,以及私有递归激活函数_activateNodeRecurisively、私有递归反激活函数_deactivateNodeRecursively。
  • component-scheduler 类主要负责 start、update、lateUpdate 周期函数的触发。

node-activator

activateNode

image

调用

  • 场景运行前会调用 runSceneImmediate函数中的scene._activate会调用_nodeActivator的activateNode函数。
  • 节点首次active=ture 时,且祖父节点已激活时,会调用
  • addChild、setParent、node.parent,且自身是激活、祖父节点已激活时,会调用

激活状态

激活状态是节点的active=true的状态。对节点树进行深度遍历,把每个节点上的所有组件存入task的__preload 中、onLoad 中、以及onEnable 中。然后依次触发队列。

  1. 存放 __preload 的队列是一个特殊的队列,该队列不会进行排序。即不受exectionOrder的影响。触发顺序与深度遍历顺序一致。
  2. onLoad、onEnable 是存放在一个会根据组件的 exectionOrder 进行排序的队列,排序方式是升序排序。加入队列时根据exectionOrder 小于0、等于0、大于0 被分别放入队列的 _neg、_zero、_pos 数组中。如在触发onLoad时,会先对这三个数组进行升序排序再调用onLoad函数,调用后立即清空数组。onEnable同理。

image

注意

__preload、onLoad、onEnable 都受节点(祖父节点)是否是激活状态决定调用(祖父节点优先级高于节点自身,因为是深度遍历)。同时onEnable 还受组件本身是否被启用(enabled)控制,即节点是激活状态(active=true),还会再判断 enabled=true。

结合节点树(cocos面板-层级管理器一栏)分析

__preload、onLoad 、onEnable 的执行顺序是从上往下(所有节点打开看)依次执行。优先把节点树中所有组件的__preload执行完,再执行节点树中所有组件的 onLoad 函数,最后执行节点树中所有组件的onEnable 函数。

结合组件是否启用分析

  • 组件是否启用不会影响_preload、onLoad 调用,即enabled值是true或false 不会影响_preload、onLoad调用。
  • onEnable、start、update、lateUpdate 函数受组件是否启用影响。

反激活状态

反激活状态就是节点的active=false。深度遍历该节点树,调用component-scheduler的disableComp方法,同时会清除start队列、update队列、lateUpdate队列中存放的该节点树下的所有的组件。会清除组件启用标志位。

activateComp 函数

该函数是负责把组件的__preload、onLoad、onEnable函数放入对应的队列中,或直接调用__preload、onLoad、onEnable 函数。

image

调用

  • 节点激活时,会调用,此时会先放入队列中,然后再触发队列。
  • 添加组件(addComponent)时,会调用。此时的调用是直接调用,即在那个函数中addComponent 即会立马触发__preload、onLoad、onEnable三个函数。

注意:loudspeaker:

例如 在onLoad中给节点添加组件(addComponent),会立即出发__preload、onLoad、onEnable三个函数,而后继续遍历后续节点的组件

onEnable 调用的特殊性

在activateComp函数中,onEnable的调用是通过调用component-scheduler的enableComp 函数,

该函数enableComp主要实现的功能:

对onEnable函数的操作

  • 把组件放入 onEnable队列中,等待_activateNodeRecurisively收集完成后,再触发onEnable队列。
  • 组件启用(enabled=true)时,会调用enableComp函数,此时是直接调用组件的onEnable函数。

把组件的start、update、lateUpdate函数放入component-scheduler的对应队列中。加入时,会进行升序排序。

注意:loudspeaker:

如果已经进入start、update、lateUpdate其中一个函数执行节点,会暂时缓存到deferredComps数组中,等待后续执行。例如:在start函数中激活组件enabled=true,此时会先暂时放入deferredComps数组中,等待start函数(紧紧是当前函数不是所有组件的start函数)执行完,再执行启用组件的start函数。启用组件的update、lateUpdate按照常规的顺序执行。

destroyComp 函数

节点被真正销毁会调用该函数。具体请查看 (Component node)isValid 与 cc.isValid 区别

该函数实现的功能:先直接调用组件的onDisable,再直接调用onDestroy。

因为正在的销毁是在 lateUpdate 与渲染函数_render 之间,所有onDisable、onDestroy发生在lateUpdate之后。

cc.Director 的mainLoop函数

start、update、lateUpdate是在activateComp函数中放入Component-scheduler的对应队列中,触发是在mainLoop函数中触发的。
image

Component-scheduler

用于管理所有脚本组件的 start、update、lateUpdate三个周期函数。创建了三个对应的调用器用于触发start、update、lateUpdate周期函数。

this.startInvoker = new OneOffInvoker(invokeStart);// 创建start调用器,用于触发组件的 start 生命周期方法 this.updateInvoker = new ReusableInvoker(invokeUpdate);// 创建更新调用器,用于触发组件的 update 生命周期方法 this.lateUpdateInvoker = new ReusableInvoker(invokeLateUpdate);// 创建lateUpdate更新调用器,用于触发组件的 lateUpdate 生命周期方法

每个调用器的数据结构类似node-activator 的onLoad、onEnable都会排序。所有的已激活启用的脚步组件的都会放入这三个调用器中,并进行升序排序。这三个调用器是在CCDirector类中的mainLoop 函数中触发的。

脚本组件加入时机:在每个脚本组件的onEnable调用时,会把脚本组件加入到这三个调用器中。

删除:当节点从激活状态=>不激活 active=false或当脚本组件从启用=>不启用时,enabled=false,会从这三个调用器中删除对应的脚步组件

第四 单个周期函数总结

onLoad

onLoad 的调用是在节点被激活时,被调用,却仅此一次。再次由未激活状态 到激活状态便不会被调用。

  1. onLoad 的调用不受组件是否被启用控制,仅由节点是否激活控制,同时也由其祖父节点是否激活控制(优先级高于节点本身,因为是深度遍历节点树的),addChild、node.parent 会立即触发,不会放入 onLoad 队列中后才触发。
  2. 结合节点树,onLoad 的调用顺序是从根节点开始的一种深度遍历的顺序进行调用。
  3. 结合节点本身,如果节点有多个组件,则是由上到下的调用顺序(编辑面板)。但是我们可以通过 exectionOrder 控制执行顺序。

注意 :loudspeaker:

我们不可以在该方法中去调用在自己节点树以下的其他组件在 onLoad、start 中做了业务逻辑处理后得出来的结果值。因为它们的 onLoad、start 方法尚未执行,无法拿到正确的结果值。但可以使用节点树中所有序列化的数据以及加载好的外部数据。

在onLoad的操作

不建议使用需要依赖其他组件的onLoad处理后的数据,根节点(如场景)除外。

__preload

同 onLoad,区别:onLoad 受 exectionOrder 影响,__preload 不受影响

start

start 的调用是脚本组件首次启用时调用(onEnable->start),仅此一次。再次由未启用状态=》启用状态便不会被调用。

  1. 永远在__preload、onLoad 函数后调用,相对于场景中的所有脚本组件而言。
  2. 首次启用时,start 会在 onEnable 后才调用,但是在 update 调用之前。
  3. 结合节点树,start 的调用顺序同 onLoad
  4. 结合节点本身,start 的调用顺序同 onLoad

注意 :loudspeaker:

我们不可以在该方法中去调用在自己节点树以下的其他组件在 start 中做了业务逻辑处理后得出来的结果值。因为它们的 start 方法尚未执行,无法拿到正确的结果值。但是我们可以去拿任意脚本组件在 onLoad 中逻辑处理后的结果值。(如果是默认就已启用,我们也可以拿到 onEnable 中的逻辑处理后的结果值,因次数 onEnable 执行在 start 前,但是一般我们不这样做,因为 onEnable 会被多次调用)。

在start中的操作

可以使用加载好的配置数据、序列化的数据、节点树中所有组件的onLoad中处理后的数据。不建议使用需要依赖其他组件的start处理后的数据,根节点除外。

onEnable

onEnable 的调用是由脚本组件是否启用,可多次触发,即由未启用状态->启用状态 会调用一次。

  1. 永远在__preload、onLoad 函数后调用,相对于所有脚本组件而言。
  2. 首次启用时,在 start 前调用
  3. 结合节点树,onEnable 的调用顺序同 onLoad
  4. 结合节点本身,onEnable 的调用顺序同 onLoad

一般我们在此函数中的操作:

  • 启动定时器,该组件启用时,启动定时器做一些业务逻辑
  • 启用碰撞检测,该组件启用时,可开启碰撞,如果禁用,则可以关闭碰撞。

onDisable

onDisable 的调用是在 脚本组件被禁用时。 可多次触发,即由启用状态->禁用状态 会调用一次。

  1. 脚本组件默认状态 enabled=false 时(面板不勾选),并不会触发 onDisable
  2. 默认状态是启用时,在 onLoad、start 中,设置不启用,也不会触发 onDisable
  3. 删除时,会触发,未启用不会触发。已启用的脚本组件执行顺序,按深度遍历顺序

一般我们在此函数中的操作:

  • 关闭启动定时器,该组件禁用时,关闭定时器
  • 关闭碰撞检测,该组件禁用时,关闭不必要的碰撞检测

update

update 是每帧都会调用的帧函数。节点未激活、在场景中未激活、脚本组件未启用时都不会触发。

  1. 在__preload、onLoad、onEnable、start 之后调用,在 lateUpdate 之前调用

lateUpdate

lateUpdate 是每帧都会调用的帧函数。节点未激活、在场景中未激活、脚本组件未启用时都不出触发。

  1. 在__preload、onLoad、onEnable、start、update 之后调用
  2. 在 lateUpdate 函数之后,会进行彻底删除调用了 destroy 的节点或组件,紧接着调用渲染函数进行渲染。

注意 :loudspeaker:: 该函数是渲染前,最后一次可以修改渲染数据的回调函数。(当前帧)

onDestroy

onDestroy 是节点被删除时调用。未激活节点上的脚本组件不会触发。节点已激活,脚本组件未启用依然会触发

  1. 触发顺序,以节点树最深、最左的脚本组件开始触发,然后从左到右,节点上的脚本组件顺序是从上往下触发

image

扩展:

isValid 与 cc.isValid 区别

  1. 当节点调用了 Destroy 方法后,会给该对象的_objFlags 添加一个 ToDestroy 标志位,并暂时存储在对象的全局变量 objectsToDestroy 数组中。对象真正被删除时,会给_objFlags 添加一个 Destroyed 标志位。

在 lateUpdate 函数触发之后,会调用 obj._deferredDestroy() 函数,删除所有存储在 objectsToDestroy 数组中的对象。对象的真正删除工作由对象自己的 _onPreDestroy 函数删除(事件、动作、定时器等,调用周期函数 onDestroy等),再添加一个 Destroyed 标志。

image
image

注意 :loudspeaker:
实际删除是在当前帧进行的,并不是下一帧,即在 lateUpdate 之后,渲染函数render之前,即两者之间删除的。(常说节点删除是在下一帧删除,因为cocos是以lateUpdate函数调用之后为一帧的结束)

总结:

  1. isValid 是检查该对象的_objFlag 是否有 Destroyed,但该对象只有在真正删除时,才存在 Destroyed。
    image

  2. cc.isValid 有两种检查模式,第二个参数为空或false时,与 isValid 一致。 第二个参数等于 true 时,则同时检测了 Destroyed、toDestroy 两个标志位,来判断对象是否被删除,推荐使用。
    image

结尾语

洋洋洒洒写了这么多,看完之后,是否让你对周期函数有了更深的认知与理解呢? 如果有帮忙留下一个mark~ , 感兴趣的同学也可以留下一个mark~ 毕竟mark了就是学习了嘛 :smile:

5赞

要是3.x版本的就更好了

:clap: :clap: 点赞 原来还有一个_preload

简单的看了一下3.x 的 差别不大