详解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(); }); }
其实核心代码只有一点点…为了保持对各个平台的兼容性,
重要的地方有两个:
- 调用ccinputmanager的方法
- 注册页面可视状态改变事件,并派发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,有以下几种种特殊情况:
- 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(负数)表示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系统事件产生及触发的资料请关注其它相关文章!
上一篇: MATLAB 如何求取离散点的曲率最大值
下一篇: Java基础之数组详解