• 售前

  • 售后

热门帖子
入门百科

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

[复制链接]
123457462 显示全部楼层 发表于 2021-10-26 12:43:03 |阅读模式 打印 上一主题 下一主题
目次


  • 情况
  • 概要

    • 模块作用
    • 涉及文件

  • 源码剖析

    • CCGame.js
    • CCInputManager.js

  • 变乱是怎么从引擎到节点的?

    • CCEventManager.js

  • 变乱是注册到了哪里?

    • event-target.js(EventTarget)
    • callbacks-invoker.js(CallbacksInvoker)

  • 变乱是怎么触发的?

    • callbacks-invoker.js

  • 末端

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

  • 总结

情况

Cocos Creator 2.4
Chrome 88

概要


模块作用

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

涉及文件


此中,CCGame和CCInputManager都有涉及注册变乱,但他们负责的是差别的部分。

源码剖析

变乱是怎么(从欣赏器)到达引擎的?
想知道这个题目,必须要了解引擎和欣赏器的交互是从何而起。
上代码。

CCGame.js
  1. // 初始化事件系统
  2. _initEvents: function () {
  3.   var win = window, hiddenPropName;
  4.   //_ register system events
  5.   // 注册系统事件,这里调用了CCInputManager的方法
  6.   if (this.config.registerSystemEvent)
  7.     _cc.inputManager.registerSystemEvent(this.canvas);
  8.   // document.hidden表示页面隐藏,后面的if用于处理浏览器兼容
  9.   if (typeof document.hidden !== 'undefined') {
  10.     hiddenPropName = "hidden";
  11.   } else if (typeof document.mozHidden !== 'undefined') {
  12.     hiddenPropName = "mozHidden";
  13.   } else if (typeof document.msHidden !== 'undefined') {
  14.     hiddenPropName = "msHidden";
  15.   } else if (typeof document.webkitHidden !== 'undefined') {
  16.     hiddenPropName = "webkitHidden";
  17.   }
  18.   // 当前页面是否隐藏
  19.   var hidden = false;
  20.   // 页面隐藏时的回调,并发射game.EVENT_HIDE事件
  21.   function onHidden () {
  22.     if (!hidden) {
  23.       hidden = true;
  24.       game.emit(game.EVENT_HIDE);
  25.     }
  26.   }
  27.   //_ In order to adapt the most of platforms the onshow API.
  28.   // 为了适配大部分平台的onshow API。应该是指传参的部分...
  29.   // 页面可视时的回调,并发射game.EVENT_SHOW事件
  30.   function onShown (arg0, arg1, arg2, arg3, arg4) {
  31.     if (hidden) {
  32.       hidden = false;
  33.       game.emit(game.EVENT_SHOW, arg0, arg1, arg2, arg3, arg4);
  34.     }
  35.   }
  36.   // 如果浏览器支持隐藏属性,则注册页面可视状态变更事件
  37.   if (hiddenPropName) {
  38.     var changeList = [
  39.       "visibilitychange",
  40.       "mozvisibilitychange",
  41.       "msvisibilitychange",
  42.       "webkitvisibilitychange",
  43.       "qbrowserVisibilityChange"
  44.     ];
  45.     // 循环注册上面的列表里的事件,同样是是为了兼容
  46.     // 隐藏状态变更后,根据可视状态调用onHidden/onShown回调函数
  47.     for (var i = 0; i < changeList.length; i++) {
  48.       document.addEventListener(changeList[i], function (event) {
  49.         var visible = document[hiddenPropName];
  50.         //_ QQ App
  51.         visible = visible || event["hidden"];
  52.         if (visible)
  53.           onHidden();
  54.         else
  55.           onShown();
  56.       });
  57.     }
  58.   }
  59.   // 此处省略部分关于 页面可视状态改变 的兼容性代码
  60.   // 注册隐藏和显示事件,暂停或重新开始游戏主逻辑。
  61.   this.on(game.EVENT_HIDE, function () {
  62.     game.pause();
  63.   });
  64.   this.on(game.EVENT_SHOW, function () {
  65.     game.resume();
  66.   });
  67. }
复制代码
实在核心代码只有一点点…为了保持对各个平台的兼容性,
告急的地方有两个:
       
  • 调用CCInputManager的方法   
  • 注册页面可视状态改变变乱,并派发game.EVENT_HIDE和game.EVENT_SHOW变乱。
来看看CCInputManager。

CCInputManager.js
  1. // 注册系统事件 element是canvas
  2. registerSystemEvent (element) {
  3.   if(this._isRegisterEvent) return;
  4.   // 注册过了,直接return
  5.   this._glView = cc.view;
  6.   let selfPointer = this;
  7.   let canvasBoundingRect = this._canvasBoundingRect;
  8.   // 监听resize事件,修改this._canvasBoundingRect
  9.   window.addEventListener('resize', this._updateCanvasBoundingRect.bind(this));
  10.   let prohibition = sys.isMobile;
  11.   let supportMouse = ('mouse' in sys.capabilities);
  12.   // 是否支持触摸
  13.   let supportTouches = ('touches' in sys.capabilities);
  14.        
  15.   // 省略了鼠标事件的注册代码
  16.   
  17.   //_register touch event
  18.   // 注册触摸事件
  19.   if (supportTouches) {
  20.     // 事件map
  21.     let _touchEventsMap = {
  22.       "touchstart": function (touchesToHandle) {
  23.         selfPointer.handleTouchesBegin(touchesToHandle);
  24.         element.focus();
  25.       },
  26.       "touchmove": function (touchesToHandle) {
  27.         selfPointer.handleTouchesMove(touchesToHandle);
  28.       },
  29.       "touchend": function (touchesToHandle) {
  30.         selfPointer.handleTouchesEnd(touchesToHandle);
  31.       },
  32.       "touchcancel": function (touchesToHandle) {
  33.         selfPointer.handleTouchesCancel(touchesToHandle);
  34.       }
  35.     };
  36.     // 遍历map注册事件
  37.     let registerTouchEvent = function (eventName) {
  38.       let handler = _touchEventsMap[eventName];
  39.       // 注册事件到canvas上
  40.       element.addEventListener(eventName, (function(event) {
  41.         if (!event.changedTouches) return;
  42.         let body = document.body;
  43.         // 计算偏移量
  44.         canvasBoundingRect.adjustedLeft = canvasBoundingRect.left - (body.scrollLeft || window.scrollX || 0);
  45.         canvasBoundingRect.adjustedTop = canvasBoundingRect.top - (body.scrollTop || window.scrollY || 0);
  46.         // 从事件中获得触摸点,并调用回调函数
  47.         handler(selfPointer.getTouchesByEvent(event, canvasBoundingRect));
  48.         // 停止事件冒泡
  49.         event.stopPropagation();
  50.         event.preventDefault();
  51.       }), false);
  52.     };
  53.     for (let eventName in _touchEventsMap) {
  54.       registerTouchEvent(eventName);
  55.     }
  56.   }
  57.   // 修改属性表示已完成事件注册
  58.   this._isRegisterEvent = true;
  59. }
复制代码
在代码中,重要完成的变乱就是注册了touchstart等一系列的原生变乱,在变乱回调中,则分别调用了selfPointer(=this)中的函数进行处置惩罚。这里我们用touchstart变乱作为例子,即handleTouchesBegin函数。
  1. // 处理touchstart事件
  2. handleTouchesBegin (touches) {
  3.   let selTouch, index, curTouch, touchID,
  4.       handleTouches = [], locTouchIntDict = this._touchesIntegerDict,
  5.       now = sys.now();
  6.   // 遍历触摸点
  7.   for (let i = 0, len = touches.length; i < len; i ++) {
  8.     // 当前触摸点
  9.     selTouch = touches[i];
  10.     // 触摸点id
  11.     touchID = selTouch.getID();
  12.     // 触摸点在触摸点列表(this._touches)中的位置
  13.     index = locTouchIntDict[touchID];
  14.     // 如果没有获得index,说明是个新的触摸点(刚按下去)
  15.     if (index == null) {
  16.       // 获得一个没有被使用的index
  17.       let unusedIndex = this._getUnUsedIndex();
  18.       // 取不到,抛出错误。可能是超出了支持的最大触摸点数量。
  19.       if (unusedIndex === -1) {
  20.         cc.logID(2300, unusedIndex);
  21.         continue;
  22.       }
  23.       //_curTouch = this._touches[unusedIndex] = selTouch;
  24.       // 存储触摸点
  25.       curTouch = this._touches[unusedIndex] = new cc.Touch(selTouch._point.x, selTouch._point.y, selTouch.getID());
  26.       curTouch._lastModified = now;
  27.       curTouch._setPrevPoint(selTouch._prevPoint);
  28.       locTouchIntDict[touchID] = unusedIndex;
  29.       // 加到需要处理的触摸点列表中
  30.       handleTouches.push(curTouch);
  31.     }
  32.   }
  33.   // 如果有新触点,生成一个触摸事件,分发到eventManager
  34.   if (handleTouches.length > 0) {
  35.     // 这个方法会把触摸点的位置根据scale做处理
  36.     this._glView._convertTouchesWithScale(handleTouches);
  37.     let touchEvent = new cc.Event.EventTouch(handleTouches);
  38.     touchEvent._eventCode = cc.Event.EventTouch.BEGAN;
  39.     eventManager.dispatchEvent(touchEvent);
  40.   }
  41. },
复制代码
函数中,一部分代码用于过滤是否有新的触摸点产生,另一部分用于处置惩罚并分发变乱(如果需要的话)。
到这里,变乱就完成了从欣赏器到引擎的转化,变乱已经到达eventManager里。那么引擎到节点之间又经历了什么?

变乱是怎么从引擎到节点的?

通报变乱到节点的工作重要都发生在CCEventManager类中。包括了存储变乱监听器,分发变乱等。先从_dispatchTouchEvent作为入口来看看。

CCEventManager.js
  1. // 分发事件
  2. _dispatchTouchEvent: function (event) {
  3.   // 为触摸监听器排序
  4.   // TOUCH_ONE_BY_ONE:触摸事件监听器类型,触点会一个一个地分开被派发
  5.   // TOUCH_ALL_AT_ONCE:触点会被一次性全部派发
  6.   this._sortEventListeners(ListenerID.TOUCH_ONE_BY_ONE);
  7.   this._sortEventListeners(ListenerID.TOUCH_ALL_AT_ONCE);
  8.   // 获得监听器列表
  9.   var oneByOneListeners = this._getListeners(ListenerID.TOUCH_ONE_BY_ONE);
  10.   var allAtOnceListeners = this._getListeners(ListenerID.TOUCH_ALL_AT_ONCE);
  11.   //_ If there aren't any touch listeners, return directly.
  12.   // 如果没有任何监听器,直接return。
  13.   if (null === oneByOneListeners && null === allAtOnceListeners)
  14.     return;
  15.   // 存储一下变量
  16.   var originalTouches = event.getTouches(), mutableTouches = cc.js.array.copy(originalTouches);
  17.   var oneByOneArgsObj = {event: event, needsMutableSet: (oneByOneListeners && allAtOnceListeners), touches: mutableTouches, selTouch: null};
  18.   //
  19.   //_ process the target handlers 1st
  20.   //  不会翻。感觉是首先处理单个触点的事件。
  21.   if (oneByOneListeners) {
  22.     // 遍历触点,依次分发
  23.     for (var i = 0; i < originalTouches.length; i++) {
  24.       event.currentTouch = originalTouches[i];
  25.       event._propagationStopped = event._propagationImmediateStopped = false;
  26.       this._dispatchEventToListeners(oneByOneListeners, this._onTouchEventCallback, oneByOneArgsObj);
  27.     }
  28.   }
  29.   //
  30.   //_ process standard handlers 2nd
  31.   //  不会翻。感觉是其次处理多触点事件(一次性全部派发)
  32.   if (allAtOnceListeners && mutableTouches.length > 0) {
  33.     this._dispatchEventToListeners(allAtOnceListeners, this._onTouchesEventCallback, {event: event, touches: mutableTouches});
  34.     if (event.isStopped())
  35.       return;
  36.   }
  37.   // 更新触摸监听器列表,主要是移除和新增监听器
  38.   this._updateTouchListeners(event);
  39. },
复制代码
函数中,重要做的变乱就是,排序、分发到注册的监听器列表、更新监听器列表。平平无奇。你大概会希奇,怎么有一个突兀的排序?哎,这正是重中之重!关于排序的作用,可以看官方文档触摸变乱的通报。正是这个排序,实现了差别层级/差别zIndex的节点之间的触点归属题目。排序会在背面提到,妙不可言。
分发变乱是通过调用_dispatchEventToListeners函数实现的,接着就来看一下它的内部实现。
  1. /**
  2. * 分发事件到监听器列表
  3. * @param {*} listeners     监听器列表
  4. * @param {*} onEvent       事件回调
  5. * @param {*} eventOrArgs   事件/参数
  6. */
  7. _dispatchEventToListeners: function (listeners, onEvent, eventOrArgs) {
  8.   // 是否需要停止继续分发
  9.   var shouldStopPropagation = false;
  10.   // 获得固定优先级的监听器(系统事件)
  11.   var fixedPriorityListeners = listeners.getFixedPriorityListeners();
  12.   // 获得场景图优先级别的监听器(我们添加的监听器正常都是在这里)
  13.   var sceneGraphPriorityListeners = listeners.getSceneGraphPriorityListeners();
  14.   /**
  15.   * 监听器触发顺序:
  16.   *      固定优先级中优先级 < 0
  17.   *      场景图优先级别
  18.   *      固定优先级中优先级 > 0
  19.   */
  20.   var i = 0, j, selListener;
  21.   if (fixedPriorityListeners) {  //_ priority < 0
  22.     if (fixedPriorityListeners.length !== 0) {
  23.       // 遍历监听器分发事件
  24.       for (; i < listeners.gt0Index; ++i) {
  25.         selListener = fixedPriorityListeners[i];
  26.         // 若 监听器激活状态 且 没有被暂停 且 已被注册到事件管理器
  27.         // 最后一个onEvent是使用_onTouchEventCallback函数分发事件到监听器
  28.         // onEvent会返回一个boolean,表示是否需要继续向后续的监听器分发事件,若true,停止继续分发
  29.         if (selListener.isEnabled() && !selListener._isPaused() && selListener._isRegistered() && onEvent(selListener, eventOrArgs)) {
  30.           shouldStopPropagation = true;
  31.           break;
  32.         }
  33.       }
  34.     }
  35.   }
  36.   // 省略另外两个优先级的触发代码
  37. },
复制代码
在函数中,通过遍历监听器列表,将变乱依次分发出去,并根据onEvent的返回值判定是否需要继续派发。一般情况下,一个触摸变乱被节点吸取到后,就会制止派发。随后会从该节点进行冒泡派发等逻辑。这也是一个重点,即触摸变乱仅有一个节点会进行响应,至于节点的优先级,就是上面提到的排序算法啦。
这里的onEvent实在是_onTouchEventCallback函数,来看看。
  1. // 触摸事件回调。分发事件到监听器
  2. _onTouchEventCallback: function (listener, argsObj) {
  3.   //_ Skip if the listener was removed.
  4.   // 若 监听器已被移除,跳过。
  5.   if (!listener._isRegistered())
  6.     return false;
  7.   var event = argsObj.event, selTouch = event.currentTouch;
  8.   event.currentTarget = listener._node;
  9.   // isClaimed:监听器是否认领事件
  10.   var isClaimed = false, removedIdx;
  11.   var getCode = event.getEventCode(), EventTouch = cc.Event.EventTouch;
  12.   // 若 事件为触摸开始事件
  13.   if (getCode === EventTouch.BEGAN) {
  14.     // 若 不支持多点触摸 且 当前已经有一个触点了
  15.     if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch) {
  16.       // 若 该触点已被节点认领 且 该节点在节点树中是激活的,则不处理事件
  17.       let node = eventManager._currentTouchListener._node;
  18.       if (node && node.activeInHierarchy) {
  19.         return false;
  20.       }
  21.     }
  22.     // 若 监听器有对应事件
  23.     if (listener.onTouchBegan) {
  24.       // 尝试分发给监听器,会返回一个boolean,表示监听器是否认领该事件
  25.       isClaimed = listener.onTouchBegan(selTouch, event);
  26.       // 若 事件被认领 且 监听器是已被注册的,保存一些数据
  27.       if (isClaimed && listener._registered) {
  28.         listener._claimedTouches.push(selTouch);
  29.         eventManager._currentTouchListener = listener;
  30.         eventManager._currentTouch = selTouch;
  31.       }
  32.     }
  33.   }
  34.   // 若 监听器已有认领的触点 且 当前触点正是被当前监听器认领
  35.   else if (listener._claimedTouches.length > 0
  36.            && ((removedIdx = listener._claimedTouches.indexOf(selTouch)) !== -1)) {
  37.     // 直接领回家
  38.     isClaimed = true;
  39.     // 若 不支持多点触摸 且 已有触点 且 已有触点还不是当前触点,不处理事件
  40.     if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch && eventManager._currentTouch !== selTouch) {
  41.       return false;
  42.     }
  43.     // 分发事件给监听器
  44.     // ENDED或CANCELED的时候,需要清理监听器和事件管理器中的触点
  45.     if (getCode === EventTouch.MOVED && listener.onTouchMoved) {
  46.       listener.onTouchMoved(selTouch, event);
  47.     } else if (getCode === EventTouch.ENDED) {
  48.       if (listener.onTouchEnded)
  49.         listener.onTouchEnded(selTouch, event);
  50.       if (listener._registered)
  51.         listener._claimedTouches.splice(removedIdx, 1);
  52.       eventManager._clearCurTouch();
  53.     } else if (getCode === EventTouch.CANCELED) {
  54.       if (listener.onTouchCancelled)
  55.         listener.onTouchCancelled(selTouch, event);
  56.       if (listener._registered)
  57.         listener._claimedTouches.splice(removedIdx, 1);
  58.       eventManager._clearCurTouch();
  59.     }
  60.   }
  61.   //_ If the event was stopped, return directly.
  62.   // 若事件已经被停止传递,直接return(对事件调用stopPropagationImmediate()等情况)
  63.   if (event.isStopped()) {
  64.     eventManager._updateTouchListeners(event);
  65.     return true;
  66.   }
  67.   // 若 事件被认领 且 监听器把事件吃掉了(x)(指不需要再继续传递,默认为false,但在Node的touch系列事件中为true)
  68.   if (isClaimed && listener.swallowTouches) {
  69.     if (argsObj.needsMutableSet)
  70.       argsObj.touches.splice(selTouch, 1);
  71.     return true;
  72.   }
  73.   return false;
  74. },
复制代码
函数重要功能是分发变乱,并对多触点进行兼容处置惩罚。告急的是返回值,当变乱被监听器认领时,就会返回true,克制变乱的继续通报。
分发变乱时,以触摸开始变乱为例,会调用监听器的onTouchBegan方法。奇了怪了,不是分发给节点嘛?为什么是调用监听器?监听器是个什么东西?这就要研究一下,当我们对节点调用on函数注册变乱的时间,变乱注册到了哪里?

变乱是注册到了哪里?

对节点调的on函数,那相干代码天然在CCNode里。直接来看看on函数都干了些啥。
  1. /**
  2. * 在节点上注册指定类型的回调函数
  3. * @param {*} type          事件类型
  4. * @param {*} callback      回调函数
  5. * @param {*} target        目标(用于绑定this)
  6. * @param {*} useCapture    注册在捕获阶段
  7. */
  8. on (type, callback, target, useCapture) {
  9.   // 是否是系统事件(鼠标、触摸)
  10.   let forDispatch = this._checknSetupSysEvent(type);
  11.   if (forDispatch) {
  12.     // 注册事件
  13.     return this._onDispatch(type, callback, target, useCapture);
  14.   }
  15.   // 省略掉非系统事件的部分,其中包括了位置改变、尺寸改变等。
  16. },
复制代码
官方表明老长一串,我给写个简化版。总之就是用来注册针对某变乱的回调函数。
你大概想说,内容这么少???然而这里分了两个分支,一个是调用_checknSetupSysEvent函数,一个是_onDispatch函数,代码都在内里555。
注册相干的是_onDispatch函数,另一个一会讲。
  1. // 注册分发事件
  2. _onDispatch (type, callback, target, useCapture) {
  3.   //_ Accept also patameters like: (type, callback, useCapture)
  4.   // 也可以接收这样的参数:(type, callback, useCapture)
  5.   // 参数兼容性处理
  6.   if (typeof target === 'boolean') {
  7.     useCapture = target;
  8.     target = undefined;
  9.   }
  10.   else useCapture = !!useCapture;
  11.   // 若 没有回调函数,报错,return。
  12.   if (!callback) {
  13.     cc.errorID(6800);
  14.     return;
  15.   }
  16.   // 根据useCapture获得不同的监听器。
  17.   var listeners = null;
  18.   if (useCapture) {
  19.     listeners = this._capturingListeners = this._capturingListeners || new EventTarget();
  20.   }
  21.   else {
  22.     listeners = this._bubblingListeners = this._bubblingListeners || new EventTarget();
  23.   }
  24.   // 若 已注册了相同的回调事件,则不做处理
  25.   if ( !listeners.hasEventListener(type, callback, target) ) {
  26.     // 注册事件到监听器
  27.     listeners.on(type, callback, target);
  28.     // 保存this到target的__eventTargets数组里,用于从target中调用targetOff函数来清除监听器。
  29.     if (target && target.__eventTargets) {
  30.       target.__eventTargets.push(this);
  31.     }
  32.   }
  33.   return callback;
  34. },
复制代码
节点会持有两个监听器,一个是_capturingListeners,一个是_bubblingListeners,区别是什么呢?前者是注册在捕获阶段的,后者是冒泡阶段,更详细的区别背面会讲。
  1. listeners.on(type, callback, target);
复制代码
可以看出实在变乱是注册在这两个监听器中的,而不在节点里。
那就看看内里是个啥玩意。

event-target.js(EventTarget)
  1. //_注册事件目标的特定事件类型回调。这种类型的事件应该被 `emit` 触发。
  2. proto.on = function (type, callback, target, once) {
  3.     // 若 没有传递回调函数,报错,return
  4.     if (!callback) {
  5.         cc.errorID(6800);
  6.         return;
  7.     }
  8.     // 若 已存在该回调,不处理
  9.     if ( !this.hasEventListener(type, callback, target) ) {
  10.         // 注册事件
  11.         this.__on(type, callback, target, once);
  12.         if (target && target.__eventTargets) {
  13.             target.__eventTargets.push(this);
  14.         }
  15.     }
  16.     return callback;
  17. };
复制代码
追到末了,又是一个on…由
  1. js.extend(EventTarget, CallbacksInvoker);
复制代码
可以看出,EventTarget继续了CallbacksInvoker,再扒一层!

callbacks-invoker.js(CallbacksInvoker)
  1. //_ 事件添加管理
  2. proto.on = function (key, callback, target, once) {
  3.     // 获得事件对应的回调列表
  4.     let list = this._callbackTable[key];
  5.     // 若 不存在,到池子里取一个
  6.     if (!list) {
  7.         list = this._callbackTable[key] = callbackListPool.get();
  8.     }
  9.     // 把回调相关信息存起来
  10.     let info = callbackInfoPool.get();
  11.     info.set(callback, target, once);
  12.     list.callbackInfos.push(info);
  13. };
复制代码
终于到头啦!此中,callbackListPool和callbackInfoPool都是js.Pool对象,这是一个对象池。回调函数终极会存储在_callbackTable中。
了解完存储的位置,那变乱又是怎么被触发的?

变乱是怎么触发的?

了解触发之前,先来看看触发顺序。先看一段官方表明。
  1. 鼠标或触摸事件会被系统调用 dispatchEvent 方法触发,触发的过程包含三个阶段:    
  2. * 1. 捕获阶段:派发事件给捕获目标(通过 [code]_getCapturingTargets
复制代码
 获取),比如,节点树中注册了捕获阶段的父节点,从根节点开始派发直到目标节点。* 2. 目标阶段:派发给目标节点的监听器。* 3. 冒泡阶段:派发变乱给冒泡目标(通过 
  1. _getBubblingTargets
复制代码
 获取),比如,节点树中注册了冒泡阶段的父节点,从目标节点开始派发直到根节点。[/code]啥意思呢?on函数的第四个参数useCapture,若为true,则变乱会被注册在捕获阶段,即可以最早被调用。
需要留意的是,捕获阶段的触发顺序是从父节点到子节点(从根节点开始)。随后会触发节点自己注册的变乱。末了,进入冒泡阶段,将变乱从父节点通报到根节点。
简朴明白:捕获阶段从上到下,然后自己,末了冒泡阶段从下到上。
理论大概有点生硬,一会看代码就懂了!
还记得_checknSetupSysEvent函数嘛,前面的表明只是写了检查是否为系统变乱,实在它做的变乱可不止这么一点点。
  1. // 检查是否是系统事件
  2. _checknSetupSysEvent (type) {
  3.   // 是否需要新增监听器
  4.   let newAdded = false;
  5.   // 是否需要分发(系统事件需要)
  6.   let forDispatch = false;
  7.   // 若 事件是触摸事件
  8.   if (_touchEvents.indexOf(type) !== -1) {
  9.     // 若 当前没有触摸事件监听器 新建一个
  10.     if (!this._touchListener) {
  11.       this._touchListener = cc.EventListener.create({
  12.         event: cc.EventListener.TOUCH_ONE_BY_ONE,
  13.         swallowTouches: true,
  14.         owner: this,
  15.         mask: _searchComponentsInParent(this, cc.Mask),
  16.         onTouchBegan: _touchStartHandler,
  17.         onTouchMoved: _touchMoveHandler,
  18.         onTouchEnded: _touchEndHandler,
  19.         onTouchCancelled: _touchCancelHandler
  20.       });
  21.       // 将监听器添加到eventManager
  22.       eventManager.addListener(this._touchListener, this);
  23.       newAdded = true;
  24.     }
  25.     forDispatch = true;
  26.   }
  27.   // 省略事件是鼠标事件的代码,和触摸事件差不多
  28.   
  29.   // 若 新增了监听器 且 当前节点不是活跃状态
  30.   if (newAdded && !this._activeInHierarchy) {
  31.     // 稍后一小会,若节点仍不是活跃状态,暂停节点的事件传递,
  32.     cc.director.getScheduler().schedule(function () {
  33.       if (!this._activeInHierarchy) {
  34.         eventManager.pauseTarget(this);
  35.       }
  36.     }, this, 0, 0, 0, false);
  37.   }
  38.   return forDispatch;
  39. },
复制代码
重点在哪呢?在
  1. eventManager.addListener(this._touchListener, this);
复制代码
这行。可以看到,每个节点都会持有一个_touchListener,并将其添加到eventManager中。是不是有点眼熟?哎,这不就是刚刚eventManager分发变乱时的玩意嘛!这不就连起来了嘛,固然eventManager不持有节点,但是持有这些监听器啊!
新建监听器的时间,传了一大堆参数,还是拿认识的触摸开始变乱,
  1. onTouchBegan: _touchStartHandler
复制代码
,这又是个啥玩意呢?
  1. // 触摸开始事件处理器
  2. var _touchStartHandler = function (touch, event) {
  3.     var pos = touch.getLocation();
  4.     var node = this.owner;
  5.     // 若 触点在节点范围内,则触发事件,并返回true,表示这事件我领走啦!
  6.     if (node._hitTest(pos, this)) {
  7.         event.type = EventType.TOUCH_START;
  8.         event.touch = touch;
  9.         event.bubbles = true;
  10.         // 分发到本节点内
  11.         node.dispatchEvent(event);
  12.         return true;
  13.     }
  14.     return false;
  15. };
复制代码
简简朴单,得到触点,判定触点是否落在节点内,是则分发!
  1. //_ 分发事件到事件流中。
  2. dispatchEvent (event) {
  3.   _doDispatchEvent(this, event);
  4.   _cachedArray.length = 0;
  5. },
  6. // 分发事件
  7. function _doDispatchEvent (owner, event) {
  8.     var target, i;
  9.     event.target = owner;
  10.     //_ Event.CAPTURING_PHASE
  11.     // 捕获阶段
  12.     _cachedArray.length = 0;
  13.     // 获得捕获阶段的节点,储存在_cachedArray
  14.     owner._getCapturingTargets(event.type, _cachedArray);
  15.     //_ capturing
  16.     event.eventPhase = 1;
  17.     // 从尾到头遍历(即从根节点到目标节点的父节点)
  18.     for (i = _cachedArray.length - 1; i >= 0; --i) {
  19.         target = _cachedArray[i];
  20.         // 若 目标节点注册了捕获阶段的监听器
  21.         if (target._capturingListeners) {
  22.             event.currentTarget = target;
  23.             //_ fire event
  24.             // 在目标节点上处理事件
  25.             target._capturingListeners.emit(event.type, event, _cachedArray);
  26.             //_ check if propagation stopped
  27.             // 若 事件已经停止传递了,return
  28.             if (event._propagationStopped) {
  29.                 _cachedArray.length = 0;
  30.                 return;
  31.             }
  32.         }
  33.     }
  34.     // 清空_cachedArray
  35.     _cachedArray.length = 0;
  36.     //_ Event.AT_TARGET
  37.     //_ checks if destroyed in capturing callbacks
  38.     // 目标节点本身阶段
  39.     event.eventPhase = 2;
  40.     event.currentTarget = owner;
  41.     // 若 自身注册了捕获阶段的监听器,则处理事件
  42.     if (owner._capturingListeners) {
  43.         owner._capturingListeners.emit(event.type, event);
  44.     }
  45.     // 若 事件没有被停止 且 自身注册了冒泡阶段的监听器,则处理事件
  46.     if (!event._propagationImmediateStopped && owner._bubblingListeners) {
  47.         owner._bubblingListeners.emit(event.type, event);
  48.     }
  49.     // 若 事件没有被停止 且 事件需要冒泡处理(默认true)
  50.     if (!event._propagationStopped && event.bubbles) {
  51.         //_ Event.BUBBLING_PHASE
  52.         // 冒泡阶段
  53.         // 获得冒泡阶段的节点
  54.         owner._getBubblingTargets(event.type, _cachedArray);
  55.         //_ propagate
  56.         event.eventPhase = 3;
  57.         // 从头到尾遍历(实现从父节点到根节点),触发逻辑和捕获阶段一致
  58.         for (i = 0; i < _cachedArray.length; ++i) {
  59.             target = _cachedArray[i];
  60.             if (target._bubblingListeners) {
  61.                 event.currentTarget = target;
  62.                 //_ fire event
  63.                 target._bubblingListeners.emit(event.type, event);
  64.                 //_ check if propagation stopped
  65.                 if (event._propagationStopped) {
  66.                     _cachedArray.length = 0;
  67.                     return;
  68.                 }
  69.             }
  70.         }
  71.     }
  72.     // 清空_cachedArray
  73.     _cachedArray.length = 0;
  74. }
复制代码
不知道看完有没有对变乱的触发顺序有更进一步的了解呢?
此中对于捕获阶段的节点和冒泡阶段的节点,是通过别的函数来得到的,用捕获阶段的代码来做示例,两者是雷同的。
  1. _getCapturingTargets (type, array) {
  2.   // 从父节点开始
  3.   var parent = this.parent;
  4.   // 若 父节点不为空(根节点的父节点为空)
  5.   while (parent) {
  6.     // 若 节点有捕获阶段的监听器 且 有对应类型的监听事件,则把节点加到array数组中
  7.     if (parent._capturingListeners && parent._capturingListeners.hasEventListener(type)) {
  8.       array.push(parent);
  9.     }
  10.     // 设置节点为其父节点
  11.     parent = parent.parent;
  12.   }
  13. },
复制代码
一个自底向上的遍历,将沿途符合条件的节点加到数组中,就得到了全部需要处置惩罚的节点!
似乎有点偏题… 回到刚刚的变乱分发,同样,因为不管是捕获阶段的监听器,还是冒泡阶段的监听器,都是一个EventTarget,这边拿自身的触发来做示例。
  1. owner._bubblingListeners.emit(event.type, event);
复制代码
上面这行代码将变乱分发到自身节点的冒泡监听器里,所以直接看看emit里是什么。
emit实在是CallbacksInvoker里的方法。

callbacks-invoker.js
  1. proto.emit = function (key, arg1, arg2, arg3, arg4, arg5) {
  2.     // 获得事件列表
  3.     const list = this._callbackTable[key];
  4.     // 若 事件列表存在
  5.     if (list) {
  6.         // list.isInvoking 事件是否正在触发
  7.         const rootInvoker = !list.isInvoking;
  8.         list.isInvoking = true;
  9.         // 获得回调列表,遍历
  10.         const infos = list.callbackInfos;
  11.         for (let i = 0, len = infos.length; i < len; ++i) {
  12.             const info = infos[i];
  13.             if (info) {
  14.                 let target = info.target;
  15.                 let callback = info.callback;
  16.                 // 若 回调函数是用once注册的,那先把这个函数取消掉
  17.                 if (info.once) {
  18.                     this.off(key, callback, target);
  19.                 }
  20.                 // 若 传递了target,则使用call保证this的指向是正确的
  21.                 if (target) {
  22.                     callback.call(target, arg1, arg2, arg3, arg4, arg5);
  23.                 }
  24.                 else {
  25.                     callback(arg1, arg2, arg3, arg4, arg5);
  26.                 }
  27.             }
  28.         }
  29.         // 若 当前事件没有在被触发
  30.         if (rootInvoker) {
  31.             list.isInvoking = false;
  32.             // 若 含有被取消的回调,则调用purgeCanceled函数,过滤已被移除的回调并压缩数组
  33.             if (list.containCanceled) {
  34.                 list.purgeCanceled();
  35.             }
  36.         }
  37.     }
  38. };
复制代码
核心是,根据变乱得到回调函数列表,遍历调用,末了根据需要做一个接纳。到此为止啦!

末端


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

前面的内容中,有提到_sortEventListeners函数,用于将监听器按照触发优先级排序,这个算法我觉得蛮风趣的,与君共赏。
先理论。节点树顾名思义肯定是个树布局。那如果树中随机取两个节点A、B,有以下几种种特别情况:
       
  • A和B属于同一个父节点   
  • A和B不属于同一个父节点   
  • A是B的某个父节点(反过来也一样)
如果要排优先级的话,应该怎么排呢?令p1 p2分别便是A B。往上走:A = A.parent
       
  • 最简朴的,直接比力_localZOrder   
  • A和B往上朔源,早晚会有一个共同的父节点,这时如果比力_localZOrder,大概有点不公平,因为大概有一个节点走了很远的路(层级更高),应该优先触发。此时又分情况:A和B层级一样。那p1 p2往上走,走到雷同父节点,比力_localZOrder即可,A层级大于B。当p走到根节点时,将p交换到另一个出发点。举例:p2会先到达根节点,此时,把p2放到A位置,继续。早晚他们会走过雷同的距离,此时父节点雷同。根据p1 p2的_localZOrder排序并取反即可。因为层级大的已经被交换到另一边了。这段要捋捋,妙不可言。   
  • 同样往上朔源,但不一样的是,因为有父子关系,在交换走过雷同距离后,p1 p2终极会在A或B节点相遇!所以此时只要判定,是在A还是在B,若A,则A层级比力低,反之一样。所以相遇的节点优先级更低。
洋洋洒洒一大堆,上代码,简便有力!
  1. // 场景图级优先级监听器的排序算法
  2. // 返回-1(负数)表示l1优先于l2,返回正数则相反,0表示相等
  3. _sortEventListenersOfSceneGraphPriorityDes: function (l1, l2) {
  4.   // 获得监听器所在的节点
  5.   let node1 = l1._getSceneGraphPriority(),
  6.       node2 = l2._getSceneGraphPriority();
  7.   // 若 监听器2为空 或 节点2为空 或 节点2不是活跃状态 或 节点2是根节点 则l1优先
  8.   if (!l2 || !node2 || !node2._activeInHierarchy || node2._parent === null)
  9.     return -1;
  10.   // 和上面的一样
  11.   else if (!l1 || !node1 || !node1._activeInHierarchy || node1._parent === null)
  12.     return 1;
  13.   // 使用p1 p2暂存节点1 节点2
  14.   // ex:我推测是 是否发生交换的意思(exchange)
  15.   let p1 = node1, p2 = node2, ex = false;
  16.   // 若 p1 p2的父节不相等 则向上朔源
  17.   while (p1._parent._id !== p2._parent._id) {
  18.     // 若 p1的爷爷节点是空(p1的父节点是根节点) 则ex置为true,p1指向节点2。否则p1指向其父节点
  19.     p1 = p1._parent._parent === null ? (ex = true) && node2 : p1._parent;
  20.     p2 = p2._parent._parent === null ? (ex = true) && node1 : p2._parent;
  21.   }
  22.   // 若 p1和p2指向同一个节点,即节点1、2存在某种父子关系,即情况3
  23.   if (p1._id === p2._id) {
  24.     // 若 p1指向节点2 则l1优先。反之l2优先
  25.     if (p1._id === node2._id)
  26.       return -1;
  27.     if (p1._id === node1._id)
  28.       return 1;
  29.   }
  30.   // 注:此时p1 p2的父节点相同
  31.   // 若ex为true 则节点1、2没有父子关系,即情况2
  32.   // 若ex为false 则节点1、2父节点相同,即情况1
  33.   return ex ? p1._localZOrder - p2._localZOrder : p2._localZOrder - p1._localZOrder;
  34. },
复制代码
总结

游戏由CCGame而起,调用CCInputManager、CCEventManager注册变乱。随后的交互里,由引擎的回调调用CCEventManager中的监听器们,再到CCNode中对于变乱的处置惩罚。若掷中,进而通报到EventTarget中存储的变乱列表,便走完了这一路。
模块实在没有到很复杂的田地,但是涉及多少文件,加上各种兼容性、安全性处置惩罚,显得多了起来。
以上就是详解CocosCreator系统变乱是怎么产生及触发的的详细内容,更多关于CocosCreator系统变乱产生及触发的资料请关注草根技术分享其它相干文章!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作