CocosCreator-Schedule计时器-设定及触发原理

计时器

JavaScript自带的定时任务

setTimeout

作用:设置一个定时器,在指定时间(毫秒)后触发,调用传入的回调函数。
参数类型:(function/code, delayTime, args…)
function: 回调函数
code: 字符串形式的代码,会使用eval()执行,因此不推荐使用
delayTime: (可选) 延迟的时间(毫秒),默认0,尽快执行。
args: (可选) 不定长参数列表,会传递给回调函数

setInterval

作用:设置一个定时器,隔一段时间触发一次,调用传入的回调函数。
参数类型:(function/code, delayTime, args…)
和sertTimeout一致,区别是setInterval会一直执行回调函数,但setTimeout**仅执行一次。**delayTime就是每次间隔的时间。

注意事项

  • 回调函数中的this可能和你期望的不太一样,所以一般会使用箭头函数/bind的方式修改this的指向
// UserInfoView.js
cc.Class({
    extends: cc.Component,
    ctor () {
      // 定义测试变量
      this.testInfo = 222;
    },
    start () {
        // 1. 箭头函数
        setTimeout(() => {
          // do something
        }, 10000);

        // 2. bind方式
        setTimeout(function() {
          // do something
          cc.log(this);								// 指向当前组件 UserInfoView{xxxxx}
          cc.log(this.testInfo);			// 222
        }.bind(this), 1000);

        // 3. 直接传递
        setTimeout(function() {
          // do something
          cc.log(this);								// 指向Window对象
          cc.log(this.testInfo);			// undefined
        }, 1000);
  }
  • 回调函数并不一定精准的按照设定的时间被触发,详情可以参考MDN的文章实际延时比设定值更久的原因:最小延迟时间。

Cocos Creator中的定时任务(计时器)

schedule

作用:结合了setTimeout和setInterval,如官方文档所说,提供了更大的灵活性,可以设置执行次数、第一次的延迟以及触发间隔时间。注:该方法定义于cc.Component,故只能在继承cc.Component的对象上调用(从编辑器创建的js类会默认继承cc.Component)。
参数列表:(callback, interval, repeat, delay)
callback: 回调函数
interval: 触发间隔(秒)
repeat: 重复次数,可设置cc.macro.REPEAT_FOREVER让它一直重复。注:实际执行次数是重复次数+1
delay: 延迟时间(秒)

设置定时器

// CCComponent.js		cocos2d\core\components\CCComponent.js
schedule (callback, interval, repeat, delay) {
  // 1619: callback function must be non-null 回调函数不能为空
  cc.assertID(callback, 1619);
	
  // 默认触发间隔为0
  interval = interval || 0;
  // 1620:interval must be positive 间隔必须是有效的(>=0)
  cc.assertID(interval >= 0, 1620);
	
  // repeat不是数字时, 默认为无限重复
  repeat = isNaN(repeat) ? cc.macro.REPEAT_FOREVER : repeat;
  // 默认延迟时间为0
  delay = delay || 0;
	
  // 获得获取系统定时器(是唯一的,cc.director是单例的,在init方法中创建了一个Scheduler定时器)
  // scheduler: cc.Scheduler类型 定义于engine/cocos2d/core/CCScheduler.js
  var scheduler = cc.director.getScheduler();
	
  // scheduler内部维护了一个叫_hashForTimers的键值对,通过传入的this可以获得一个HashTimerEntry对象(定义于CCScheduler)
  // HashTimerEntry中保存了paused属性(boolean),当组件_onDisabled/_onEnabled时,paused会被设置为true/false
  // 也有可能是_hashForUpdates。_hashForTimers保存的是自定义定时器的信息,_hashForUpdates保存的是update定时器的信息
  var paused = scheduler.isTargetPaused(this);
	
  // 调用CCScheduler中的方法,这里传递的target为this,所以回调函数不需要bind/使用箭头函数
  scheduler.schedule(callback, this, interval, repeat, delay, paused);
}
// CCScheduler.js		cocos2d\core\CCScheduler.js
schedule: function (callback, target, interval, repeat, delay, paused) {
  'use strict';
  if (typeof callback !== 'function') {
  	// 交换callback和target的值
    var tmp = callback;
    callback = target;
    target = tmp;
  }
  
  // 适配不同参数长度
  //selector, target, interval, repeat, delay, paused
  //selector, target, interval, paused
  if (arguments.length === 4 || arguments.length === 5) {
    paused = !!repeat;
    repeat = cc.macro.REPEAT_FOREVER;
    delay = 0;
  }
	
  // 1502:cc.scheduler.scheduleCallbackForTarget(): target should be non-null. target不能为空
  cc.assertID(target, 1502);
	
  // 获得组件的id
  var targetId = target._id;
  if (!targetId) {
    if (target.__instanceId) {
      // 1513:cc.Scheduler: scheduler stopped using `__instanceId` as id since v2.0, you should do scheduler.enableForTarget(target) before all scheduler API usage on target
      // 提示2.0版本后不再使用__instanceId属性 这里应该是为了保持兼容
      cc.warnID(1513);		
      targetId = target._id = target.__instanceId;
    }
    else {
      // 1510:cc.Scheduler: Illegal target which doesn't have uuid or instanceId.
      // target非法,没有uuid和instanceId
      cc.errorID(1510);
    }
  }
  
  // 通过targetId获得对应的定时器实例(HashTimerEntry),有点拗口的感觉,里面保存了timers target paused等属性 
  var element = this._hashForTimers[targetId];
  if (!element) {
    // 没有取到实例 使用get方法从对象池(_hashTimerEntries)中获得一个实例
    // Is this the 1st element ? Then set the pause level to all the callback_fns of this target
    element = HashTimerEntry.get(null, target, 0, null, null, paused);
    this._arrayForTimers.push(element);
    this._hashForTimers[targetId] = element;
  } else if (element.paused !== paused) {
    // 1511:cc.Scheduler: pause state of the scheduled task doesn't match the element pause state in Scheduler, the given paused state will be ignored.
    // 定时器实例中的paused属性和传入的paused不匹配,传入的值会被忽略(可能在创建的过程中,组件disable/enable状态改变了)
    cc.warnID(1511);
  }

  var timer, i;
  // 定时器列表(timers)为空 初始化为空数组
  if (element.timers == null) {
    element.timers = [];
  }
  else {
    // 遍历列表 判断是否有相同的回调函数
    for (i = 0; i < element.timers.length; ++i) {
      timer = element.timers[i];
      if (timer && callback === timer._callback) {
        // 1507:CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %s to %s"
        // 回调已存在,将更新interval属性
        cc.logID(1507, timer.getInterval(), interval);
        timer._interval = interval;
        return;
      }
    }
  }
	
  // 获得一个定时器实例(CallbackTimer),这里也有一个对象池(_timers)
  timer = CallbackTimer.get();
  // 初始化 里面的代码基本类似 _callback = callback 这样的赋值
  timer.initWithCallback(this, callback, target, interval, repeat, delay);
  // 加到定时器列表中
  element.timers.push(timer);
	
  // 修改_currentTargetSalvaged防止当前的HashTimerEntry被删除 这个在update函数中会有相关解释
  if (this._currentTarget === element && this._currentTargetSalvaged) {
    this._currentTargetSalvaged = false;
  }
}

以上就是创建计时器的代码,感觉许多代码量都在做一些验证、兼容之类的操作,最后创建计时器相关对象储存起来,而触发回调的代码则是在update函数中

Scheduler的update触发逻辑

// scheduler的update函数在cc.director.mainLoop()中触发
// CCDirector.js		cocos2d\core\CCDirector.js
this._scheduler.update(this._deltaTime);

// 而director的mainLoop函数在cc.game._runMainLoop()中触发
// CCGame.js				cocos2d\core\CCGame.js
_runMainLoop: function () {
  // 省略部分代码
  callback = function (now) {
    if (!self._paused) {
      self._intervalId = window.requestAnimFrame(callback);
      if (!CC_JSB && !CC_RUNTIME && frameRate === 30) {
        if (skip = !skip) {
          return;
        }
      }
      // 在这~
      director.mainLoop(now);
    }
  };
	
  // 使用requestAnimFrame设置一个每帧触发的回调,会在下一次的重绘之前被调用,一般来说60次/秒。
  // 详情可参考MDN文档https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
  self._intervalId = window.requestAnimFrame(callback);
  self._paused = false;
}

Scheduler中的update函数

// CCScheduler.js		cocos2d\core\CCScheduler.js
/**
     * !#en 'update' the scheduler. (You should NEVER call this method, unless you know what you are doing.)
     * !#zh update 调度函数。(不应该直接调用这个方法,除非完全了解这么做的结果)
     * @method update
     * @param {Number} dt delta time
     */
update: function (dt) {
  // boolean属性 加锁 避免执行update定时器过程中出现删除操作
  this._updateHashLocked = true;
  // 时间缩放 可以实现慢/快动作
  if(this._timeScale !== 1)
    dt *= this._timeScale;

  var i, list, len, entry;
	
  // 此处省略触发update定时器的代码

  // Iterate over all the custom selectors
  // 遍历自定义定时器
  var elt, arr = this._arrayForTimers;
  for(i=0; i<arr.length; i++){
    elt = arr[i];
    this._currentTarget = elt;
    this._currentTargetSalvaged = false;
		
    // 目标没有被停止则执行
    if (!elt.paused){
      // The 'timers' array may change while inside this loop
      // 遍历定时器列表。列表有可能在循环过程中被改变(repeat次数执行完了)
      for (elt.timerIndex = 0; elt.timerIndex < elt.timers.length; ++(elt.timerIndex)){
        elt.currentTimer = elt.timers[elt.timerIndex];
        elt.currentTimerSalvaged = false;
				
        // 调用定时器的update函数
        elt.currentTimer.update(dt);
        elt.currentTimer = null;
      }
    }

    // only delete currentTarget if no actions were scheduled during the cycle (issue #481)
    // _currentTargetSalvaged在上面被设为false,但仍有可能在update函数执行后被改变(repeat次数执行完了)
    // 但是不会直接回收,而是通过设置_currentTargetSalvaged,在这里统一回收。(避免了重复回收?)
    if (this._currentTargetSalvaged && this._currentTarget.timers.length === 0) {
      this._removeHashElement(this._currentTarget);
      --i;
    }
  }

  // 此处省略清理update定时器的代码
	
  // 解除锁
  this._updateHashLocked = false;
  this._currentTarget = null;
}

CallbackTimer中的update函数

// CallbackTimer		定义于cocos2d\core\CCScheduler.js
/**
 * triggers the timer
 * @param {Number} dt delta time
 */
proto.update = function (dt) {
  	// _elapsed: boolean 离上一次触发的事件 调用CallbackTimer.initWithCallback()的时候会被设为-1
    if (this._elapsed === -1) {
      	// 第一次触发的时候 重置_elapsed和_timesExecuted,(所以定时器是在下一帧开始才正式生效的?)
        this._elapsed = 0;
        this._timesExecuted = 0;
    } else {
      	// 累加_elapsed 实现delay和interval的效果
        this._elapsed += dt;
     		// 当前计时器是一直循环的且不需要延迟
        if (this._runForever && !this._useDelay) {//standard timer usage
          	// 标准触发流程
          	// _elapsed达到interval间隔时间,触发事件,重置_elapsed
            if (this._elapsed >= this._interval) {
                this.trigger();
                this._elapsed = 0;
            }
        } else {//advanced usage
          	// 高级用法(?)
            if (this._useDelay) {
          			// 需要延迟 达到延迟时间的时候触发
                if (this._elapsed >= this._delay) {
                    this.trigger();
										
                  	// 扣除延迟的时间、已执行次数+1、重置延迟状态为false
                    this._elapsed -= this._delay;
                    this._timesExecuted += 1;
                    this._useDelay = false;
                }
            } else {
              	// 不需要延迟(或延迟已经触发过了) 达到间隔时间触发
                if (this._elapsed >= this._interval) {
                    this.trigger();
										
                  	// 重置_elapsed、已执行次数+1
                    this._elapsed = 0;
                    this._timesExecuted += 1;
                }
            }

          	// 设置了repeat次数的定时器,执行次数大于_repeat次数的时候 取消这个定时器
          	// 这就是上面的update中提到的定时器列表有可能被改变
            if (this._callback && !this._runForever && this._timesExecuted > this._repeat)
                this.cancel();
        }
    }
};
// 触发回调
proto.trigger = function () {
    if (this._target && this._callback) {
      	// 加锁 放回对象池中的时候会判断_lock是否为true
        this._lock = true;
      	// 调用回调函数
        this._callback.call(this._target, this._elapsed);
        this._lock = false;
    }
};

使用方法

start () {
    this.testInfo = 222;
		
    this.schedule((dt)=>{
      // 每0.1s执行一次
      cc.log("回调1", dt);
    }, 0.1);

    this.schedule((dt)=>{
      // 每0.1s执行一次,重复一次(总共执行两次)
      cc.log("回调2", dt);
    }, 0.1, 1);

    this.schedule((dt)=>{
      // 每0.1s执行一次,不重复,延迟0.2秒后触发
      cc.log("回调3", dt);
      cc.log("测试this", this.testInfo);
    }, 0.1, 0, 0.2);
		
    this.schedule((dt)=>{
      // 0.5s后触发,测试active对计时器的影响
      cc.log("回调4", dt);
      this.node.active = false;
    }, 0.5);
}

// 输出1
// 回调1 0.10002300000000003
// 回调2 0.10002300000000003
// 回调1 0.10004099999999994
// 回调2 0.10004099999999994
// 回调3 0.200064
// 测试this 222
// 回调1 0.11671000000000005
// 回调1 0.10003199999999993
// 回调4 0.5000120000000001

// 输出2
// 回调1 0.10002899999999999
// 回调2 0.10002899999999999
// 回调3 0.20002500000000004
// 测试this 222
// 回调1 0.11670800000000003
// 回调2 0.11670800000000003
// 回调1 0.116745
// 回调1 0.11657899999999996
// 回调4 0.5001940000000001

一些结论

  • 相对于js自带的定时任务,schedule实现了重复固定次数、延迟执行等功能,也解决了this指向的问题。
  • 因为引擎底层做了参数兼容性处理,所以可以传入任意长度的参数。如当只传入一个参数的时候,定时器会被一直触发。
  • 定时器执行结果不一定是一致的,在多次测试中,出现了上面两种输出结果,获得的dt也可能是不一致的,何况update计时器会优先于自定义计时器触发,所以cocos creator提供的计时器也不是准确的。
  • 节点的active会影响计时器的执行(这不是显然的吗…),即源码中的_paused属性。

参考链接

  1. MDN Window.setTimeout Window.setInterval
  2. MDN 实际延时比设定值更久的原因:最小延迟时间
  3. Cocos Creator Api文档 Component.schedule

一些后话

秋招挺凄惨的,想去的都挂了。主要原因还是自己以前对基础知识不够重视,这个结局算是还债吧。
因为一些原因,开始复习的也比较晚。复(yu)习的时候就想顺便写点东西记录一下。
文章是真的难写啊淦(x 很怕写错。
应该是第一次相对深入的读cocos creator的源码,还挺有意思的。但是毕竟还是小菜鸡,有纰漏还望大家多多包涵2333。

你可能感兴趣的:(CocosCreator,cocos-creator,javascript)