详解CocosCreator系统事件是怎么产生及触发的

环境

Cocos Creator 2.4
Chrome 88

概要

模块作用

事件监听机制应该是所有游戏都必不可少的内容。不管是按钮的点击还是物体的拖动,都少不了事件的监听与分发。
主要的功能还是通过节点的on/once函数,对系统事件(如触摸、点击)进行监听,随后触发对应的游戏逻辑。同时,也支持用户发射/监听自定义的事件,这方面可以看一下官方文档监听和发射事件。

涉及文件

其中,CCGame和CCInputManager都有涉及注册事件,但他们负责的是不同的部分。

源码解析

事件是怎么(从浏览器)到达引擎的?

想知道这个问题,必须要了解引擎和浏览器的交互是从何而起。
上代码。

CCGame.js

// 初始化事件系统
_initEvents: function () {
  var win = window, hiddenPropName;

  //_ register system events
  // 注册系统事件,这里调用了CCInputManager的方法
  if (this.config.registerSystemEvent)
    _cc.inputManager.registerSystemEvent(this.canvas);

  // document.hidden表示页面隐藏,后面的if用于处理浏览器兼容
  if (typeof document.hidden !== 'undefined') {
    hiddenPropName = "hidden";
  } else if (typeof document.mozHidden !== 'undefined') {
    hiddenPropName = "mozHidden";
  } else if (typeof document.msHidden !== 'undefined') {
    hiddenPropName = "msHidden";
  } else if (typeof document.webkitHidden !== 'undefined') {
    hiddenPropName = "webkitHidden";
  }

  // 当前页面是否隐藏
  var hidden = false;

  // 页面隐藏时的回调,并发射game.EVENT_HIDE事件
  function onHidden () {
    if (!hidden) {
      hidden = true;
      game.emit(game.EVENT_HIDE);
    }
  }
  //_ In order to adapt the most of platforms the onshow API.
  // 为了适配大部分平台的onshow API。应该是指传参的部分...
  // 页面可视时的回调,并发射game.EVENT_SHOW事件
  function onShown (arg0, arg1, arg2, arg3, arg4) {
    if (hidden) {
      hidden = false;
      game.emit(game.EVENT_SHOW, arg0, arg1, arg2, arg3, arg4);
    }
  }

  // 如果浏览器支持隐藏属性,则注册页面可视状态变更事件
  if (hiddenPropName) {
    var changeList = [
      "visibilitychange",
      "mozvisibilitychange",
      "msvisibilitychange",
      "webkitvisibilitychange",
      "qbrowserVisibilityChange"
    ];
    // 循环注册上面的列表里的事件,同样是是为了兼容
    // 隐藏状态变更后,根据可视状态调用onHidden/onShown回调函数
    for (var i = 0; i < changeList.length; i++) {
      document.addEventListener(changeList[i], function (event) {
        var visible = document[hiddenPropName];
        //_ QQ App
        visible = visible || event["hidden"];
        if (visible)
          onHidden();
        else
          onShown();
      });
    }
  }
  // 此处省略部分关于 页面可视状态改变 的兼容性代码

  // 注册隐藏和显示事件,暂停或重新开始游戏主逻辑。
  this.on(game.EVENT_HIDE, function () {
    game.pause();
  });
  this.on(game.EVENT_SHOW, function () {
    game.resume();
  });
}

其实核心代码只有一点点…为了保持对各个平台的兼容性,
重要的地方有两个:

  1. 调用CCInputManager的方法
  2. 注册页面可视状态改变事件,并派发game.EVENT_HIDE和game.EVENT_SHOW事件。

来看看CCInputManager。

CCInputManager.js

// 注册系统事件 element是canvas
registerSystemEvent (element) {
  if(this._isRegisterEvent) return;

  // 注册过了,直接return
  this._glView = cc.view;
  let selfPointer = this;
  let canvasBoundingRect = this._canvasBoundingRect;

  // 监听resize事件,修改this._canvasBoundingRect
  window.addEventListener('resize', this._updateCanvasBoundingRect.bind(this));

  let prohibition = sys.isMobile;
  let supportMouse = ('mouse' in sys.capabilities);
  // 是否支持触摸
  let supportTouches = ('touches' in sys.capabilities);

  // 省略了鼠标事件的注册代码

  //_register touch event
  // 注册触摸事件
  if (supportTouches) {
    // 事件map
    let _touchEventsMap = {
      "touchstart": function (touchesToHandle) {
        selfPointer.handleTouchesBegin(touchesToHandle);
        element.focus();
      },
      "touchmove": function (touchesToHandle) {
        selfPointer.handleTouchesMove(touchesToHandle);
      },
      "touchend": function (touchesToHandle) {
        selfPointer.handleTouchesEnd(touchesToHandle);
      },
      "touchcancel": function (touchesToHandle) {
        selfPointer.handleTouchesCancel(touchesToHandle);
      }
    };

    // 遍历map注册事件
    let registerTouchEvent = function (eventName) {
      let handler = _touchEventsMap[eventName];
      // 注册事件到canvas上
      element.addEventListener(eventName, (function(event) {
        if (!event.changedTouches) return;
        let body = document.body;

        // 计算偏移量
        canvasBoundingRect.adjustedLeft = canvasBoundingRect.left - (body.scrollLeft || window.scrollX || 0);
        canvasBoundingRect.adjustedTop = canvasBoundingRect.top - (body.scrollTop || window.scrollY || 0);
        // 从事件中获得触摸点,并调用回调函数
        handler(selfPointer.getTouchesByEvent(event, canvasBoundingRect));
        // 停止事件冒泡
        event.stopPropagation();
        event.preventDefault();
      }), false);
    };
    for (let eventName in _touchEventsMap) {
      registerTouchEvent(eventName);
    }
  }

  // 修改属性表示已完成事件注册
  this._isRegisterEvent = true;
}

在代码中,主要完成的事情就是注册了touchstart等一系列的原生事件,在事件回调中,则分别调用了selfPointer(=this)中的函数进行处理。这里我们用touchstart事件作为例子,即handleTouchesBegin函数。

// 处理touchstart事件
handleTouchesBegin (touches) {
  let selTouch, index, curTouch, touchID,
      handleTouches = [], locTouchIntDict = this._touchesIntegerDict,
      now = sys.now();
  // 遍历触摸点
  for (let i = 0, len = touches.length; i < len; i ++) {
    // 当前触摸点
    selTouch = touches[i];
    // 触摸点id
    touchID = selTouch.getID();
    // 触摸点在触摸点列表(this._touches)中的位置
    index = locTouchIntDict[touchID];

    // 如果没有获得index,说明是个新的触摸点(刚按下去)
    if (index == null) {
      // 获得一个没有被使用的index
      let unusedIndex = this._getUnUsedIndex();
      // 取不到,抛出错误。可能是超出了支持的最大触摸点数量。
      if (unusedIndex === -1) {
        cc.logID(2300, unusedIndex);
        continue;
      }
      //_curTouch = this._touches[unusedIndex] = selTouch;
      // 存储触摸点
      curTouch = this._touches[unusedIndex] = new cc.Touch(selTouch._point.x, selTouch._point.y, selTouch.getID());
      curTouch._lastModified = now;
      curTouch._setPrevPoint(selTouch._prevPoint);
      locTouchIntDict[touchID] = unusedIndex;
      // 加到需要处理的触摸点列表中
      handleTouches.push(curTouch);
    }
  }
  // 如果有新触点,生成一个触摸事件,分发到eventManager
  if (handleTouches.length > 0) {
    // 这个方法会把触摸点的位置根据scale做处理
    this._glView._convertTouchesWithScale(handleTouches);
    let touchEvent = new cc.Event.EventTouch(handleTouches);
    touchEvent._eventCode = cc.Event.EventTouch.BEGAN;
    eventManager.dispatchEvent(touchEvent);
  }
},

函数中,一部分代码用于过滤是否有新的触摸点产生,另一部分用于处理并分发事件(如果需要的话)。
到这里,事件就完成了从浏览器到引擎的转化,事件已经到达eventManager里。那么引擎到节点之间又经历了什么?

事件是怎么从引擎到节点的?

传递事件到节点的工作主要都发生在CCEventManager类中。包括了存储事件监听器,分发事件等。先从_dispatchTouchEvent作为入口来看看。

CCEventManager.js

// 分发事件
_dispatchTouchEvent: function (event) {
  // 为触摸监听器排序
  // TOUCH_ONE_BY_ONE:触摸事件监听器类型,触点会一个一个地分开被派发
  // TOUCH_ALL_AT_ONCE:触点会被一次性全部派发
  this._sortEventListeners(ListenerID.TOUCH_ONE_BY_ONE);
  this._sortEventListeners(ListenerID.TOUCH_ALL_AT_ONCE);

  // 获得监听器列表
  var oneByOneListeners = this._getListeners(ListenerID.TOUCH_ONE_BY_ONE);
  var allAtOnceListeners = this._getListeners(ListenerID.TOUCH_ALL_AT_ONCE);

  //_ If there aren't any touch listeners, return directly.
  // 如果没有任何监听器,直接return。
  if (null === oneByOneListeners && null === allAtOnceListeners)
    return;

  // 存储一下变量
  var originalTouches = event.getTouches(), mutableTouches = cc.js.array.copy(originalTouches);
  var oneByOneArgsObj = {event: event, needsMutableSet: (oneByOneListeners && allAtOnceListeners), touches: mutableTouches, selTouch: null};

  //
  //_ process the target handlers 1st
  //  不会翻。感觉是首先处理单个触点的事件。
  if (oneByOneListeners) {
    // 遍历触点,依次分发
    for (var i = 0; i < originalTouches.length; i++) {
      event.currentTouch = originalTouches[i];
      event._propagationStopped = event._propagationImmediateStopped = false;
      this._dispatchEventToListeners(oneByOneListeners, this._onTouchEventCallback, oneByOneArgsObj);
    }
  }

  //
  //_ process standard handlers 2nd
  //  不会翻。感觉是其次处理多触点事件(一次性全部派发)
  if (allAtOnceListeners && mutableTouches.length > 0) {
    this._dispatchEventToListeners(allAtOnceListeners, this._onTouchesEventCallback, {event: event, touches: mutableTouches});
    if (event.isStopped())
      return;
  }
  // 更新触摸监听器列表,主要是移除和新增监听器
  this._updateTouchListeners(event);
},

函数中,主要做的事情就是,排序、分发到注册的监听器列表、更新监听器列表。平平无奇。你可能会奇怪,怎么有一个突兀的排序?哎,这正是重中之重!关于排序的作用,可以看官方文档触摸事件的传递。正是这个排序,实现了不同层级/不同zIndex的节点之间的触点归属问题。排序会在后面提到,妙不可言。
分发事件是通过调用_dispatchEventToListeners函数实现的,接着就来看一下它的内部实现。

/**
* 分发事件到监听器列表
* @param {*} listeners     监听器列表
* @param {*} onEvent       事件回调
* @param {*} eventOrArgs   事件/参数
*/
_dispatchEventToListeners: function (listeners, onEvent, eventOrArgs) {
  // 是否需要停止继续分发
  var shouldStopPropagation = false;
  // 获得固定优先级的监听器(系统事件)
  var fixedPriorityListeners = listeners.getFixedPriorityListeners();
  // 获得场景图优先级别的监听器(我们添加的监听器正常都是在这里)
  var sceneGraphPriorityListeners = listeners.getSceneGraphPriorityListeners();

  /**
  * 监听器触发顺序:
  *      固定优先级中优先级 < 0
  *      场景图优先级别
  *      固定优先级中优先级 > 0
  */
  var i = 0, j, selListener;
  if (fixedPriorityListeners) {  //_ priority < 0
    if (fixedPriorityListeners.length !== 0) {
      // 遍历监听器分发事件
      for (; i < listeners.gt0Index; ++i) {
        selListener = fixedPriorityListeners[i];
        // 若 监听器激活状态 且 没有被暂停 且 已被注册到事件管理器
        // 最后一个onEvent是使用_onTouchEventCallback函数分发事件到监听器
        // onEvent会返回一个boolean,表示是否需要继续向后续的监听器分发事件,若true,停止继续分发
        if (selListener.isEnabled() && !selListener._isPaused() && selListener._isRegistered() && onEvent(selListener, eventOrArgs)) {
          shouldStopPropagation = true;
          break;
        }
      }
    }
  }
  // 省略另外两个优先级的触发代码
},

在函数中,通过遍历监听器列表,将事件依次分发出去,并根据onEvent的返回值判定是否需要继续派发。一般情况下,一个触摸事件被节点接收到后,就会停止派发。随后会从该节点进行冒泡派发等逻辑。这也是一个重点,即触摸事件仅有一个节点会进行响应,至于节点的优先级,就是上面提到的排序算法啦。
这里的onEvent其实是_onTouchEventCallback函数,来看看。

// 触摸事件回调。分发事件到监听器
_onTouchEventCallback: function (listener, argsObj) {
  //_ Skip if the listener was removed.
  // 若 监听器已被移除,跳过。
  if (!listener._isRegistered())
    return false;

  var event = argsObj.event, selTouch = event.currentTouch;
  event.currentTarget = listener._node;

  // isClaimed:监听器是否认领事件
  var isClaimed = false, removedIdx;
  var getCode = event.getEventCode(), EventTouch = cc.Event.EventTouch;
  // 若 事件为触摸开始事件
  if (getCode === EventTouch.BEGAN) {
    // 若 不支持多点触摸 且 当前已经有一个触点了
    if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch) {
      // 若 该触点已被节点认领 且 该节点在节点树中是激活的,则不处理事件
      let node = eventManager._currentTouchListener._node;
      if (node && node.activeInHierarchy) {
        return false;
      }
    }

    // 若 监听器有对应事件
    if (listener.onTouchBegan) {
      // 尝试分发给监听器,会返回一个boolean,表示监听器是否认领该事件
      isClaimed = listener.onTouchBegan(selTouch, event);
      // 若 事件被认领 且 监听器是已被注册的,保存一些数据
      if (isClaimed && listener._registered) {
        listener._claimedTouches.push(selTouch);
        eventManager._currentTouchListener = listener;
        eventManager._currentTouch = selTouch;
      }
    }
  }
  // 若 监听器已有认领的触点 且 当前触点正是被当前监听器认领
  else if (listener._claimedTouches.length > 0
           && ((removedIdx = listener._claimedTouches.indexOf(selTouch)) !== -1)) {
    // 直接领回家
    isClaimed = true;

    // 若 不支持多点触摸 且 已有触点 且 已有触点还不是当前触点,不处理事件
    if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch && eventManager._currentTouch !== selTouch) {
      return false;
    }

    // 分发事件给监听器
    // ENDED或CANCELED的时候,需要清理监听器和事件管理器中的触点
    if (getCode === EventTouch.MOVED && listener.onTouchMoved) {
      listener.onTouchMoved(selTouch, event);
    } else if (getCode === EventTouch.ENDED) {
      if (listener.onTouchEnded)
        listener.onTouchEnded(selTouch, event);
      if (listener._registered)
        listener._claimedTouches.splice(removedIdx, 1);
      eventManager._clearCurTouch();
    } else if (getCode === EventTouch.CANCELED) {
      if (listener.onTouchCancelled)
        listener.onTouchCancelled(selTouch, event);
      if (listener._registered)
        listener._claimedTouches.splice(removedIdx, 1);
      eventManager._clearCurTouch();
    }
  }

  //_ If the event was stopped, return directly.
  // 若事件已经被停止传递,直接return(对事件调用stopPropagationImmediate()等情况)
  if (event.isStopped()) {
    eventManager._updateTouchListeners(event);
    return true;
  }

  // 若 事件被认领 且 监听器把事件吃掉了(x)(指不需要再继续传递,默认为false,但在Node的touch系列事件中为true)
  if (isClaimed && listener.swallowTouches) {
    if (argsObj.needsMutableSet)
      argsObj.touches.splice(selTouch, 1);
    return true;
  }
  return false;
},

函数主要功能是分发事件,并对多触点进行兼容处理。重要的是返回值,当事件被监听器认领时,就会返回true,阻止事件的继续传递。
分发事件时,以触摸开始事件为例,会调用监听器的onTouchBegan方法。奇了怪了,不是分发给节点嘛?为什么是调用监听器?监听器是个什么东西?这就要研究一下,当我们对节点调用on函数注册事件的时候,事件注册到了哪里?

事件是注册到了哪里?

对节点调的on函数,那相关代码自然在CCNode里。直接来看看on函数都干了些啥。

/**
* 在节点上注册指定类型的回调函数
* @param {*} type          事件类型
* @param {*} callback      回调函数
* @param {*} target        目标(用于绑定this)
* @param {*} useCapture    注册在捕获阶段
*/
on (type, callback, target, useCapture) {
  // 是否是系统事件(鼠标、触摸)
  let forDispatch = this._checknSetupSysEvent(type);
  if (forDispatch) {
    // 注册事件
    return this._onDispatch(type, callback, target, useCapture);
  }
  // 省略掉非系统事件的部分,其中包括了位置改变、尺寸改变等。
},

官方注释老长一串,我给写个简化版。总之就是用来注册针对某事件的回调函数。
你可能想说,内容这么少???然而这里分了两个分支,一个是调用_checknSetupSysEvent函数,一个是_onDispatch函数,代码都在里面555。
注册相关的是_onDispatch函数,另一个一会讲。

// 注册分发事件
_onDispatch (type, callback, target, useCapture) {
  //_ Accept also patameters like: (type, callback, useCapture)
  // 也可以接收这样的参数:(type, callback, useCapture)
  // 参数兼容性处理
  if (typeof target === 'boolean') {
    useCapture = target;
    target = undefined;
  }
  else useCapture = !!useCapture;
  // 若 没有回调函数,报错,return。
  if (!callback) {
    cc.errorID(6800);
    return;
  }

  // 根据useCapture获得不同的监听器。
  var listeners = null;
  if (useCapture) {
    listeners = this._capturingListeners = this._capturingListeners || new EventTarget();
  }
  else {
    listeners = this._bubblingListeners = this._bubblingListeners || new EventTarget();
  }

  // 若 已注册了相同的回调事件,则不做处理
  if ( !listeners.hasEventListener(type, callback, target) ) {
    // 注册事件到监听器
    listeners.on(type, callback, target);

    // 保存this到target的__eventTargets数组里,用于从target中调用targetOff函数来清除监听器。
    if (target && target.__eventTargets) {
      target.__eventTargets.push(this);
    }
  }

  return callback;
},

节点会持有两个监听器,一个是_capturingListeners,一个是_bubblingListeners,区别是什么呢?前者是注册在捕获阶段的,后者是冒泡阶段,更具体的区别后面会讲。
listeners.on(type, callback, target);可以看出其实事件是注册在这两个监听器中的,而不在节点里。
那就看看里面是个啥玩意。

event-target.js(EventTarget)

//_注册事件目标的特定事件类型回调。这种类型的事件应该被 `emit` 触发。
proto.on = function (type, callback, target, once) {
    // 若 没有传递回调函数,报错,return
    if (!callback) {
        cc.errorID(6800);
        return;
    }

    // 若 已存在该回调,不处理
    if ( !this.hasEventListener(type, callback, target) ) {
        // 注册事件
        this.__on(type, callback, target, once);

        if (target && target.__eventTargets) {
            target.__eventTargets.push(this);
        }
    }
    return callback;
};

追到最后,又是一个on…由js.extend(EventTarget, CallbacksInvoker);可以看出,EventTarget继承了CallbacksInvoker,再扒一层!

callbacks-invoker.js(CallbacksInvoker)

//_ 事件添加管理
proto.on = function (key, callback, target, once) {
    // 获得事件对应的回调列表
    let list = this._callbackTable[key];
    // 若 不存在,到池子里取一个
    if (!list) {
        list = this._callbackTable[key] = callbackListPool.get();
    }
    // 把回调相关信息存起来
    let info = callbackInfoPool.get();
    info.set(callback, target, once);
    list.callbackInfos.push(info);
};

终于到头啦!其中,callbackListPool和callbackInfoPool都是js.Pool对象,这是一个对象池。回调函数最终会存储在_callbackTable中。
了解完存储的位置,那事件又是怎么被触发的?

事件是怎么触发的?

了解触发之前,先来看看触发顺序。先看一段官方注释。

鼠标或触摸事件会被系统调用 dispatchEvent 方法触发,触发的过程包含三个阶段:    
* 1. 捕获阶段:派发事件给捕获目标(通过 _getCapturingTargets 获取),比如,节点树中注册了捕获阶段的父节点,从根节点开始派发直到目标节点。
* 2. 目标阶段:派发给目标节点的监听器。
* 3. 冒泡阶段:派发事件给冒泡目标(通过 _getBubblingTargets 获取),比如,节点树中注册了冒泡阶段的父节点,从目标节点开始派发直到根节点。

啥意思呢?on函数的第四个参数useCapture,若为true,则事件会被注册在捕获阶段,即可以最早被调用。
需要注意的是,捕获阶段的触发顺序是从父节点到子节点(从根节点开始)。随后会触发节点本身注册的事件。最后,进入冒泡阶段,将事件从父节点传递到根节点。
简单理解:捕获阶段从上到下,然后本身,最后冒泡阶段从下到上。
理论可能有点生硬,一会看代码就懂了!
还记得_checknSetupSysEvent函数嘛,前面的注释只是写了检查是否为系统事件,其实它做的事情可不止这么一点点。

// 检查是否是系统事件
_checknSetupSysEvent (type) {
  // 是否需要新增监听器
  let newAdded = false;
  // 是否需要分发(系统事件需要)
  let forDispatch = false;
  // 若 事件是触摸事件
  if (_touchEvents.indexOf(type) !== -1) {
    // 若 当前没有触摸事件监听器 新建一个
    if (!this._touchListener) {
      this._touchListener = cc.EventListener.create({
        event: cc.EventListener.TOUCH_ONE_BY_ONE,
        swallowTouches: true,
        owner: this,
        mask: _searchComponentsInParent(this, cc.Mask),
        onTouchBegan: _touchStartHandler,
        onTouchMoved: _touchMoveHandler,
        onTouchEnded: _touchEndHandler,
        onTouchCancelled: _touchCancelHandler
      });
      // 将监听器添加到eventManager
      eventManager.addListener(this._touchListener, this);
      newAdded = true;
    }
    forDispatch = true;
  }
  // 省略事件是鼠标事件的代码,和触摸事件差不多

  // 若 新增了监听器 且 当前节点不是活跃状态
  if (newAdded && !this._activeInHierarchy) {
    // 稍后一小会,若节点仍不是活跃状态,暂停节点的事件传递,
    cc.director.getScheduler().schedule(function () {
      if (!this._activeInHierarchy) {
        eventManager.pauseTarget(this);
      }
    }, this, 0, 0, 0, false);
  }
  return forDispatch;
},

重点在哪呢?在eventManager.addListener(this._touchListener, this);这行。可以看到,每个节点都会持有一个_touchListener,并将其添加到eventManager中。是不是有点眼熟?哎,这不就是刚刚eventManager分发事件时的玩意嘛!这不就连起来了嘛,虽然eventManager不持有节点,但是持有这些监听器啊!
新建监听器的时候,传了一大堆参数,还是拿熟悉的触摸开始事件,onTouchBegan: _touchStartHandler,这又是个啥玩意呢?

// 触摸开始事件处理器
var _touchStartHandler = function (touch, event) {
    var pos = touch.getLocation();
    var node = this.owner;

    // 若 触点在节点范围内,则触发事件,并返回true,表示这事件我领走啦!
    if (node._hitTest(pos, this)) {
        event.type = EventType.TOUCH_START;
        event.touch = touch;
        event.bubbles = true;
        // 分发到本节点内
        node.dispatchEvent(event);
        return true;
    }
    return false;
};

简简单单,获得触点,判断触点是否落在节点内,是则分发!

//_ 分发事件到事件流中。
dispatchEvent (event) {
  _doDispatchEvent(this, event);
  _cachedArray.length = 0;
},
// 分发事件
function _doDispatchEvent (owner, event) {
    var target, i;
    event.target = owner;

    //_ Event.CAPTURING_PHASE
    // 捕获阶段
    _cachedArray.length = 0;
    // 获得捕获阶段的节点,储存在_cachedArray
    owner._getCapturingTargets(event.type, _cachedArray);
    //_ capturing
    event.eventPhase = 1;
    // 从尾到头遍历(即从根节点到目标节点的父节点)
    for (i = _cachedArray.length - 1; i >= 0; --i) {
        target = _cachedArray[i];
        // 若 目标节点注册了捕获阶段的监听器
        if (target._capturingListeners) {
            event.currentTarget = target;
            //_ fire event
            // 在目标节点上处理事件
            target._capturingListeners.emit(event.type, event, _cachedArray);
            //_ check if propagation stopped
            // 若 事件已经停止传递了,return
            if (event._propagationStopped) {
                _cachedArray.length = 0;
                return;
            }
        }
    }
    // 清空_cachedArray
    _cachedArray.length = 0;

    //_ Event.AT_TARGET
    //_ checks if destroyed in capturing callbacks
    // 目标节点本身阶段
    event.eventPhase = 2;
    event.currentTarget = owner;
    // 若 自身注册了捕获阶段的监听器,则处理事件
    if (owner._capturingListeners) {
        owner._capturingListeners.emit(event.type, event);
    }
    // 若 事件没有被停止 且 自身注册了冒泡阶段的监听器,则处理事件
    if (!event._propagationImmediateStopped && owner._bubblingListeners) {
        owner._bubblingListeners.emit(event.type, event);
    }

    // 若 事件没有被停止 且 事件需要冒泡处理(默认true)
    if (!event._propagationStopped && event.bubbles) {
        //_ Event.BUBBLING_PHASE
        // 冒泡阶段
        // 获得冒泡阶段的节点
        owner._getBubblingTargets(event.type, _cachedArray);
        //_ propagate
        event.eventPhase = 3;
        // 从头到尾遍历(实现从父节点到根节点),触发逻辑和捕获阶段一致
        for (i = 0; i < _cachedArray.length; ++i) {
            target = _cachedArray[i];
            if (target._bubblingListeners) {
                event.currentTarget = target;
                //_ fire event
                target._bubblingListeners.emit(event.type, event);
                //_ check if propagation stopped
                if (event._propagationStopped) {
                    _cachedArray.length = 0;
                    return;
                }
            }
        }
    }
    // 清空_cachedArray
    _cachedArray.length = 0;
}

不知道看完有没有对事件的触发顺序有更进一步的了解呢?
其中对于捕获阶段的节点和冒泡阶段的节点,是通过别的函数来获得的,用捕获阶段的代码来做示例,两者是类似的。

_getCapturingTargets (type, array) {
  // 从父节点开始
  var parent = this.parent;
  // 若 父节点不为空(根节点的父节点为空)
  while (parent) {
    // 若 节点有捕获阶段的监听器 且 有对应类型的监听事件,则把节点加到array数组中
    if (parent._capturingListeners && parent._capturingListeners.hasEventListener(type)) {
      array.push(parent);
    }
    // 设置节点为其父节点
    parent = parent.parent;
  }
},

一个自底向上的遍历,将沿途符合条件的节点加到数组中,就得到了所有需要处理的节点!
好像有点偏题… 回到刚刚的事件分发,同样,因为不管是捕获阶段的监听器,还是冒泡阶段的监听器,都是一个EventTarget,这边拿自身的触发来做示例。
owner._bubblingListeners.emit(event.type, event);
上面这行代码将事件分发到自身节点的冒泡监听器里,所以直接看看emit里是什么。
emit其实是CallbacksInvoker里的方法。

callbacks-invoker.js

proto.emit = function (key, arg1, arg2, arg3, arg4, arg5) {
    // 获得事件列表
    const list = this._callbackTable[key];
    // 若 事件列表存在
    if (list) {
        // list.isInvoking 事件是否正在触发
        const rootInvoker = !list.isInvoking;
        list.isInvoking = true;

        // 获得回调列表,遍历
        const infos = list.callbackInfos;
        for (let i = 0, len = infos.length; i < len; ++i) {
            const info = infos[i];
            if (info) {
                let target = info.target;
                let callback = info.callback;
                // 若 回调函数是用once注册的,那先把这个函数取消掉
                if (info.once) {
                    this.off(key, callback, target);
                }

                // 若 传递了target,则使用call保证this的指向是正确的
                if (target) {
                    callback.call(target, arg1, arg2, arg3, arg4, arg5);
                }
                else {
                    callback(arg1, arg2, arg3, arg4, arg5);
                }
            }
        }
        // 若 当前事件没有在被触发
        if (rootInvoker) {
            list.isInvoking = false;
            // 若 含有被取消的回调,则调用purgeCanceled函数,过滤已被移除的回调并压缩数组
            if (list.containCanceled) {
                list.purgeCanceled();
            }
        }
    }
};

核心是,根据事件获得回调函数列表,遍历调用,最后根据需要做一个回收。到此为止啦!

结尾

加点有意思的监听器排序算法

前面的内容中,有提到_sortEventListeners函数,用于将监听器按照触发优先级排序,这个算法我觉得蛮有趣的,与君共赏。
先理论。节点树顾名思义肯定是个树结构。那如果树中随机取两个节点A、B,有以下几种种特殊情况:

  1. A和B属于同一个父节点
  2. A和B不属于同一个父节点
  3. A是B的某个父节点(反过来也一样)

如果要排优先级的话,应该怎么排呢?令p1 p2分别等于A B。往上走:A = A.parent

  1. 最简单的,直接比较_localZOrder
  2. A和B往上朔源,早晚会有一个共同的父节点,这时如果比较_localZOrder,可能有点不公平,因为可能有一个节点走了很远的路(层级更高),应该优先触发。此时又分情况:A和B层级一样。那p1 p2往上走,走到相同父节点,比较_localZOrder即可,A层级大于B。当p走到根节点时,将p交换到另一个起点。举例:p2会先到达根节点,此时,把p2放到A位置,继续。早晚他们会走过相同的距离,此时父节点相同。根据p1 p2的_localZOrder排序并取反即可。因为层级大的已经被交换到另一边了。这段要捋捋,妙不可言。
  3. 同样往上朔源,但不一样的是,因为有父子关系,在交换走过相同距离后,p1 p2最终会在A或B节点相遇!所以此时只要判断,是在A还是在B,若A,则A层级比较低,反之一样。所以相遇的节点优先级更低。

洋洋洒洒一大堆,上代码,简洁有力!

// 场景图级优先级监听器的排序算法
// 返回-1(负数)表示l1优先于l2,返回正数则相反,0表示相等
_sortEventListenersOfSceneGraphPriorityDes: function (l1, l2) {
  // 获得监听器所在的节点
  let node1 = l1._getSceneGraphPriority(),
      node2 = l2._getSceneGraphPriority();

  // 若 监听器2为空 或 节点2为空 或 节点2不是活跃状态 或 节点2是根节点 则l1优先
  if (!l2 || !node2 || !node2._activeInHierarchy || node2._parent === null)
    return -1;
  // 和上面的一样
  else if (!l1 || !node1 || !node1._activeInHierarchy || node1._parent === null)
    return 1;

  // 使用p1 p2暂存节点1 节点2
  // ex:我推测是 是否发生交换的意思(exchange)
  let p1 = node1, p2 = node2, ex = false;
  // 若 p1 p2的父节不相等 则向上朔源
  while (p1._parent._id !== p2._parent._id) {
    // 若 p1的爷爷节点是空(p1的父节点是根节点) 则ex置为true,p1指向节点2。否则p1指向其父节点
    p1 = p1._parent._parent === null ? (ex = true) && node2 : p1._parent;
    p2 = p2._parent._parent === null ? (ex = true) && node1 : p2._parent;
  }

  // 若 p1和p2指向同一个节点,即节点1、2存在某种父子关系,即情况3
  if (p1._id === p2._id) {
    // 若 p1指向节点2 则l1优先。反之l2优先
    if (p1._id === node2._id)
      return -1;
    if (p1._id === node1._id)
      return 1;
  }

  // 注:此时p1 p2的父节点相同
  // 若ex为true 则节点1、2没有父子关系,即情况2
  // 若ex为false 则节点1、2父节点相同,即情况1
  return ex ? p1._localZOrder - p2._localZOrder : p2._localZOrder - p1._localZOrder;
},

总结

游戏由CCGame而起,调用CCInputManager、CCEventManager注册事件。随后的交互里,由引擎的回调调用CCEventManager中的监听器们,再到CCNode中对于事件的处理。若命中,进而传递到EventTarget中存储的事件列表,便走完了这一路。
模块其实没有到很复杂的地步,但是涉及若干文件,加上各种兼容性、安全性处理,显得多了起来。

以上就是详解CocosCreator系统事件是怎么产生及触发的的详细内容,更多关于CocosCreator系统事件产生及触发的资料请关注我们其它相关文章!

(0)

相关推荐

  • CocosCreator入门教程之网络通信

    网络通信概况 开发一个网络游戏,难免要处理网络通信.有几点问题需要注意: 1.服务端为了快速开发可能前期使用http通信,后面再改成websocket/socket. 2.同时存在http和websocket/socket通信 3.通信数据格式可能需要随时替换为json或protocol buffer,可能需要添加额外头部. 4.在新手引导里使用本地数据做模拟,请求不需要发送给后端. 5.重连后数据简单同步 具体实施 针对第一.二个问题,我在设计通信模块时会考虑将通信尽量抽象为统一的对外接口,在

  • 怎样在CocosCreator中使用游戏手柄

    一.场景布置 二. 添加手柄监听器 1.监听事件的变化 由原先的mouse系列的转换为touch系列的 touchstart 触摸按下,相当于 mousedown touchmove 触摸移动,相当于 mousemove touchend 触摸抬起,相当于 mouseup touchcancel 触摸取消,被其他事件终止,相当于按下了ESC键 2.坐标设定 当触摸按下随推动位置变化(要用世界坐标转换),触摸抬起后回归原位(直接设定0,0坐标默认相对坐标). setPosition()设定的为相对

  • Cocos2d-x入门教程(详细的实例和讲解)

    智能终端上的游戏目前风头正劲,试问哪个智能手机上没有几款企鹅公司出品的游戏呢!之前从未涉猎过游戏开发,但知道游戏开发前要挑选一款合适的游戏引擎,自己从头开始敲代码的时代已经out了.在寻觅游戏引擎之前,我需要回答三道摆在我面前的选择题: 1.2D引擎还是3D引擎?    2.平台专用引擎还是跨平台引擎?    3.收费引擎还是开源引擎? 作为入门级选手,2D游戏显然更适合上手一些,另外适合果果这个年龄段的幼教类的游戏也多以2D游戏居多.3D游戏本身也太难了,不仅要 Programming能力,还

  • 详解CocosCreator消息分发机制

    概述 本篇开始介绍游戏业务架构相关的内容.在游戏业务层,所有需要隔离的系统和模块间通信都可以通过消息分发解耦.例如网络返回通知.数据更新同步到界面等. 消息分发基于观察者模式设计.需要处理消息的地方向消息中心注册监听回调,派发消息时,调用消息中心的派发接口遍历该消息的监听队列,调用对应的回调方法. 具体方案 先定义监听回调类型 /** * 消息监听回调方法 */ export type NotifyListener = (src: any, data: any) => void; 通过key-v

  • 详解CocosCreator制作射击游戏

    场景布置 游戏资源 炮塔旋转 机制与之前手柄实例的小车相同,使用touchmove监听触摸事件, 获取触摸位置 通过位置用signAngle方法将该位置与cc.v2(1,0)位置的角度差求出(记得要加负号,比较所得逆时针为负,赋值angle逆指针为正). 所求的的角度即为最终角度. onLoad(){ //初始化为90度 this.node.angle=90; this.node.on('touchstart',this.onTouchStart,this); this.node.on('tou

  • Cocos2d-x 3.x入门教程(一):基础概念

    前言 接触游戏开发时间不长,之前一直都是写Lua,写Lua肯定是没有什么发展的啦,至少你要会写的.写客户端,肯定要看看Cocos2d-x了.从今天起,正式开始Cocos2d-X的学习之旅. 之前一直都是做C++开发的,转过来看Cocos2d-x,从语言上来说,没有多少问题.对于我来说,Cocos2d-x就是一个类库,就类似于MFC.ATL和QT一样,就是一个游戏类库,而我就按照学习类库使用的方法去学习Cocos2d-x.这篇文章叫基础概念,但是还是从整体来说说Cocos2d-x这个类库的. 基础

  • CocosCreator入门教程之用TS制作第一个游戏

    前提 无论学什么技术知识,官方文档都应该是你第一个教程,所以请先至少阅读新手上路这一节 http://docs.cocos.com/creator/manual/zh/getting-started/ 再来看这篇文章. 这里假设你已经安装成功了 Cocos Creator. TypeScript VS JavaScript 这里当然只会讲优点: 1. ts 是 js 的超集,所有 js 的语法 ts 都支持. 2. ts 支持接近完美的代码提示,js 代码提示接近于没有. 3. ts 有类型定义

  • 详解CocosCreator华容道数字拼盘

    前言 华容道是啥玩意? 这种数字拼图游戏大家都玩过吧,他就是典型的华容道之一. 华容道是古老的中国民间益智游戏,以其变化多端.百玩不厌的特点与魔方.独立钻石棋一起被国外智力专家并称为"智力游戏界的三个不可思议". 今天咱们就来了解一下这个华容道. 正文 今天咱们主要以3*3的布局来进行,菜鸟用cocos creator 写了一个简单的demo,下面咱们逐步说一下 1.面板 首先咱们随机生成一个面板排列 2.华容道求解 思路: 穷举法:大家都知道这种游戏的玩法,滑动其中可以滑动的方格,将

  • 如何在CocosCreator里画个炫酷的雷达图

    前言 雷达图(Radar Chart) 也称为网络图.星图或蜘蛛网图. 是以从同一点开始的轴上表示的三个或更多个定量变量的二维图表的形式显示多元数据的图形方法. 适用于显示三个或更多的维度的变量. 雷达图常用于

  • 如何用CocosCreator制作微信小游戏

    CocosCreator在1.8版本开始,就支持一键发布微信小程序,下面是详细的发布步骤: 1.在微信公众平台下载微信开发者工具  地址: https://mp.weixin.qq.com/debug/wxagame/dev/devtools/download.html?t=2018115,根据需要选择相应的版本. 2.cocoscreator设置 选择Cocos Creator --> 偏好设置 --> 原生开发环境,配置WechatGame程序路径,点击保存并关闭. 3.配置构建发布 下面

  • 详解CocosCreator MVC架构

    概述 这一篇将介绍在游戏客户端常用的架构MVC架构.一个游戏的MVC如下划分: M:1)单例全局的数据中心World,所有游戏模块的数据在World中有入口,2)各个模块自己的数据结构. V:1)通过creator预制体制作的UI界面.场景,2)各个界面显示逻辑的ViewCtrl C:1)全局的MainCtrl,2)各个模块的业务逻辑类ModuleCtrl 具体模块 先介绍M部分.由于一个模块的数据,在其他模块也有访问的需求,例如好友模块,在聊天的时候也需要访问,在排行榜里需要访问.数据应该有一

  • Cocos2d-x 3.x入门教程(二):Node节点类

    千里之行,始于足下 上一篇文章<菜鸟学习Cocos2d-x 3.x--基础概念>中介绍了以下Cocos2d-x的基本结构和一些重要的类,而这里开始对这些重要的类进行单独的介绍与分析.开始吧~~~ Node节点类 Node类是Cocos2d-x中一个非常重要的类,它继承自类Ref,关于Ref类,Ref类是一个内存管理的类,我后续也会总结的,这里就不做多说了. Node类在Cocos2d-x中有多重要呢?任何需要画在屏幕上的对象都是节点类.最常用的节点类包括场景类(Scene).布景层类(Laye

随机推荐