全球短讯!介绍CocosCreator系统事件是怎么产生及触发的

2023-06-22 20:09:46    来源:博客园

这篇文章主要介绍了CocosCreator系统事件是怎么产生及触发的,虽然内容不少,但是只要一点点抽丝剥茧,具体分析其内容,就会豁然开朗

目录
  • 环境
  • 概要
    • 模块作用
    • 涉及文件
  • 源码解析
    • CCGame.js
    • CCInputManager.js
  • 事件是怎么从引擎到节点的?
    • CCEventManager.js
  • 事件是注册到了哪里?
    • event-target.js(EventTarget)
    • callbacks-invoker.js(CallbacksInvoker)
  • 事件是怎么触发的?
    • callbacks-invoker.js
  • 结尾
    • 加点有意思的监听器排序算法
  • 总结

环境

CocosCreator 2.4Chrome 88


【资料图】

概要

模块作用

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

涉及文件

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

源码解析

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

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

CCGame.js

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273// 初始化事件系统_initEvents:function() {varwin = window, hiddenPropName;//_ register system events// 注册系统事件,这里调用了CCInputManager的方法if(this.config.registerSystemEvent)_cc.inputManager.registerSystemEvent(this.canvas);// document.hidden表示页面隐藏,后面的if用于处理浏览器兼容if(typeofdocument.hidden !=="undefined") {hiddenPropName ="hidden";}elseif(typeofdocument.mozHidden !=="undefined") {hiddenPropName ="mozHidden";}elseif(typeofdocument.msHidden !=="undefined") {hiddenPropName ="msHidden";}elseif(typeofdocument.webkitHidden !=="undefined") {hiddenPropName ="webkitHidden";}// 当前页面是否隐藏varhidden =false;// 页面隐藏时的回调,并发射game.EVENT_HIDE事件functiononHidden () {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事件functiononShown (arg0, arg1, arg2, arg3, arg4) {if(hidden) {hidden =false;game.emit(game.EVENT_SHOW, arg0, arg1, arg2, arg3, arg4);}}// 如果浏览器支持隐藏属性,则注册页面可视状态变更事件if(hiddenPropName) {varchangeList = ["visibilitychange","mozvisibilitychange","msvisibilitychange","webkitvisibilitychange","qbrowserVisibilityChange"];// 循环注册上面的列表里的事件,同样是是为了兼容// 隐藏状态变更后,根据可视状态调用onHidden/onShown回调函数for(vari = 0; i < changeList.length; i++) {document.addEventListener(changeList[i],function(event) {varvisible = document[hiddenPropName];//_ QQ Appvisible = visible || event["hidden"];if(visible)onHidden();elseonShown();});}}// 此处省略部分关于 页面可视状态改变 的兼容性代码// 注册隐藏和显示事件,暂停或重新开始游戏主逻辑。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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465// 注册系统事件 element是canvasregisterSystemEvent (element) {if(this._isRegisterEvent)return;// 注册过了,直接returnthis._glView = cc.view;let selfPointer =this;let canvasBoundingRect =this._canvasBoundingRect;// 监听resize事件,修改this._canvasBoundingRectwindow.addEventListener("resize",this._updateCanvasBoundingRect.bind(this));let prohibition = sys.isMobile;let supportMouse = ("mouse"insys.capabilities);// 是否支持触摸let supportTouches = ("touches"insys.capabilities);// 省略了鼠标事件的注册代码//_register touch event// 注册触摸事件if(supportTouches) {// 事件maplet _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 eventNamein_touchEventsMap) {registerTouchEvent(eventName);}}// 修改属性表示已完成事件注册this._isRegisterEvent =true;}

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

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

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

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

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

CCEventManager.js

1234567891011121314151617181920212223242526272829303132333435363738394041424344// 分发事件_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);// 获得监听器列表varoneByOneListeners =this._getListeners(ListenerID.TOUCH_ONE_BY_ONE);varallAtOnceListeners =this._getListeners(ListenerID.TOUCH_ALL_AT_ONCE);//_ If there aren"t any touch listeners, returndirectly.// 如果没有任何监听器,直接return。if(null=== oneByOneListeners &&null=== allAtOnceListeners)return;// 存储一下变量varoriginalTouches = event.getTouches(), mutableTouches = cc.js.array.copy(originalTouches);varoneByOneArgsObj = {event: event, needsMutableSet: (oneByOneListeners && allAtOnceListeners), touches: mutableTouches, selTouch:null};////_ process the target handlers 1st// 不会翻。感觉是首先处理单个触点的事件。if(oneByOneListeners) {// 遍历触点,依次分发for(vari = 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函数实现的,接着就来看一下它的内部实现。

1234567891011121314151617181920212223242526272829303132333435363738/*** 分发事件到监听器列表* @param {*} listeners 监听器列表* @param {*} onEvent 事件回调* @param {*} eventOrArgs 事件/参数*/_dispatchEventToListeners:function(listeners, onEvent, eventOrArgs) {// 是否需要停止继续分发varshouldStopPropagation =false;// 获得固定优先级的监听器(系统事件)varfixedPriorityListeners = listeners.getFixedPriorityListeners();// 获得场景图优先级别的监听器(我们添加的监听器正常都是在这里)varsceneGraphPriorityListeners = listeners.getSceneGraphPriorityListeners();/*** 监听器触发顺序:* 固定优先级中优先级 < 0* 场景图优先级别* 固定优先级中优先级 > 0*/vari = 0, j, selListener;if(fixedPriorityListeners) {//_ priority < 0if(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函数,来看看。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081// 触摸事件回调。分发事件到监听器_onTouchEventCallback:function(listener, argsObj) {//_ Skip if the listener was removed.// 若 监听器已被移除,跳过。if(!listener._isRegistered())returnfalse;varevent = argsObj.event, selTouch = event.currentTouch;event.currentTarget = listener._node;// isClaimed:监听器是否认领事件varisClaimed =false, removedIdx;vargetCode = 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) {returnfalse;}}// 若 监听器有对应事件if(listener.onTouchBegan) {// 尝试分发给监听器,会返回一个boolean,表示监听器是否认领该事件isClaimed = listener.onTouchBegan(selTouch, event);// 若 事件被认领 且 监听器是已被注册的,保存一些数据if(isClaimed && listener._registered) {listener._claimedTouches.push(selTouch);eventManager._currentTouchListener = listener;eventManager._currentTouch = selTouch;}}}// 若 监听器已有认领的触点 且 当前触点正是被当前监听器认领elseif(listener._claimedTouches.length > 0&& ((removedIdx = listener._claimedTouches.indexOf(selTouch)) !== -1)) {// 直接领回家isClaimed =true;// 若 不支持多点触摸 且 已有触点 且 已有触点还不是当前触点,不处理事件if(!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch && eventManager._currentTouch !== selTouch) {returnfalse;}// 分发事件给监听器// ENDED或CANCELED的时候,需要清理监听器和事件管理器中的触点if(getCode === EventTouch.MOVED && listener.onTouchMoved) {listener.onTouchMoved(selTouch, event);}elseif(getCode === EventTouch.ENDED) {if(listener.onTouchEnded)listener.onTouchEnded(selTouch, event);if(listener._registered)listener._claimedTouches.splice(removedIdx, 1);eventManager._clearCurTouch();}elseif(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);returntrue;}// 若 事件被认领 且 监听器把事件吃掉了(x)(指不需要再继续传递,默认为false,但在Node的touch系列事件中为true)if(isClaimed && listener.swallowTouches) {if(argsObj.needsMutableSet)argsObj.touches.splice(selTouch, 1);returntrue;}returnfalse;},

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

事件是注册到了哪里?

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

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

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

1234567891011121314151617181920212223242526272829303132333435363738// 注册分发事件_onDispatch (type, callback, target, useCapture) {//_ Accept also patameters like: (type, callback, useCapture)// 也可以接收这样的参数:(type, callback, useCapture)// 参数兼容性处理if(typeoftarget ==="boolean") {useCapture = target;target = undefined;}elseuseCapture = !!useCapture;// 若 没有回调函数,报错,return。if(!callback) {cc.errorID(6800);return;}// 根据useCapture获得不同的监听器。varlisteners =null;if(useCapture) {listeners =this._capturingListeners =this._capturingListeners ||newEventTarget();}else{listeners =this._bubblingListeners =this._bubblingListeners ||newEventTarget();}// 若 已注册了相同的回调事件,则不做处理if( !listeners.hasEventListener(type, callback, target) ) {// 注册事件到监听器listeners.on(type, callback, target);// 保存this到target的__eventTargets数组里,用于从target中调用targetOff函数来清除监听器。if(target && target.__eventTargets) {target.__eventTargets.push(this);}}returncallback;},

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

event-target.js(EventTarget)

12345678910111213141516171819//_注册事件目标的特定事件类型回调。这种类型的事件应该被 `emit` 触发。proto.on =function(type, callback, target, once) {// 若 没有传递回调函数,报错,returnif(!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);}}returncallback;};

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

callbacks-invoker.js(CallbacksInvoker)

12345678910111213//_ 事件添加管理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函数嘛,前面的注释只是写了检查是否为系统事件,其实它做的事情可不止这么一点点。

123456789101112131415161718192021222324252627282930313233343536373839// 检查是否是系统事件_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});// 将监听器添加到eventManagereventManager.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);}returnforDispatch;},

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

12345678910111213141516// 触摸开始事件处理器var_touchStartHandler =function(touch, event) {varpos = touch.getLocation();varnode =this.owner;// 若 触点在节点范围内,则触发事件,并返回true,表示这事件我领走啦!if(node._hitTest(pos,this)) {event.type = EventType.TOUCH_START;event.touch = touch;event.bubbles =true;// 分发到本节点内node.dispatchEvent(event);returntrue;}returnfalse;};

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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677//_ 分发事件到事件流中。dispatchEvent (event) {_doDispatchEvent(this, event);_cachedArray.length = 0;},// 分发事件function_doDispatchEvent (owner, event) {vartarget, i;event.target = owner;//_ Event.CAPTURING_PHASE// 捕获阶段_cachedArray.length = 0;// 获得捕获阶段的节点,储存在_cachedArrayowner._getCapturingTargets(event.type, _cachedArray);//_ capturingevent.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// 若 事件已经停止传递了,returnif(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);//_ propagateevent.eventPhase = 3;// 从头到尾遍历(实现从父节点到根节点),触发逻辑和捕获阶段一致for(i = 0; i < _cachedArray.length; ++i) {target = _cachedArray[i];if(target._bubblingListeners) {event.currentTarget = target;//_ fire eventtarget._bubblingListeners.emit(event.type, event);//_ check if propagation stoppedif(event._propagationStopped) {_cachedArray.length = 0;return;}}}}// 清空_cachedArray_cachedArray.length = 0;}

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

12345678910111213_getCapturingTargets (type, array) {// 从父节点开始varparent =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

12345678910111213141516171819202122232425262728293031323334353637383940proto.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层级比较低,反之一样。所以相遇的节点优先级更低。

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

1234567891011121314151617181920212223242526272829303132333435363738// 场景图级优先级监听器的排序算法// 返回-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;// 和上面的一样elseif(!l1 || !node1 || !node1._activeInHierarchy || node1._parent ===null)return1;// 使用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存在某种父子关系,即情况3if(p1._id === p2._id) {// 若 p1指向节点2 则l1优先。反之l2优先if(p1._id === node2._id)return-1;if(p1._id === node1._id)return1;}// 注:此时p1 p2的父节点相同// 若ex为true 则节点1、2没有父子关系,即情况2// 若ex为false 则节点1、2父节点相同,即情况1returnex ? p1._localZOrder - p2._localZOrder : p2._localZOrder - p1._localZOrder;},

总结

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

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

原文链接:https://www.mimisucai.com/teach/javascript/29767.html

关键词: