詳解CocosCreator系統(tǒng)事件是怎么產(chǎn)生及觸發(fā)的
環(huán)境
Cocos Creator 2.4
Chrome 88
概要
模塊作用
事件監(jiān)聽機(jī)制應(yīng)該是所有游戲都必不可少的內(nèi)容。不管是按鈕的點(diǎn)擊還是物體的拖動(dòng),都少不了事件的監(jiān)聽與分發(fā)。
主要的功能還是通過節(jié)點(diǎn)的on/once函數(shù),對系統(tǒng)事件(如觸摸、點(diǎn)擊)進(jìn)行監(jiān)聽,隨后觸發(fā)對應(yīng)的游戲邏輯。同時(shí),也支持用戶發(fā)射/監(jiān)聽自定義的事件,這方面可以看一下官方文檔監(jiān)聽和發(fā)射事件。
涉及文件

其中,CCGame和CCInputManager都有涉及注冊事件,但他們負(fù)責(zé)的是不同的部分。
源碼解析
事件是怎么(從瀏覽器)到達(dá)引擎的?
想知道這個(gè)問題,必須要了解引擎和瀏覽器的交互是從何而起。
上代碼。
CCGame.js
// 初始化事件系統(tǒng)
_initEvents: function () {
var win = window, hiddenPropName;
//_ register system events
// 注冊系統(tǒng)事件,這里調(diào)用了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";
}
// 當(dāng)前頁面是否隱藏
var hidden = false;
// 頁面隱藏時(shí)的回調(diào),并發(fā)射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.
// 為了適配大部分平臺(tái)的onshow API。應(yīng)該是指傳參的部分...
// 頁面可視時(shí)的回調(diào),并發(fā)射game.EVENT_SHOW事件
function onShown (arg0, arg1, arg2, arg3, arg4) {
if (hidden) {
hidden = false;
game.emit(game.EVENT_SHOW, arg0, arg1, arg2, arg3, arg4);
}
}
// 如果瀏覽器支持隱藏屬性,則注冊頁面可視狀態(tài)變更事件
if (hiddenPropName) {
var changeList = [
"visibilitychange",
"mozvisibilitychange",
"msvisibilitychange",
"webkitvisibilitychange",
"qbrowserVisibilityChange"
];
// 循環(huán)注冊上面的列表里的事件,同樣是是為了兼容
// 隱藏狀態(tài)變更后,根據(jù)可視狀態(tài)調(diào)用onHidden/onShown回調(diào)函數(shù)
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();
});
}
}
// 此處省略部分關(guān)于 頁面可視狀態(tài)改變 的兼容性代碼
// 注冊隱藏和顯示事件,暫?;蛑匦麻_始游戲主邏輯。
this.on(game.EVENT_HIDE, function () {
game.pause();
});
this.on(game.EVENT_SHOW, function () {
game.resume();
});
}
其實(shí)核心代碼只有一點(diǎn)點(diǎn)…為了保持對各個(gè)平臺(tái)的兼容性,
重要的地方有兩個(gè):
- 調(diào)用CCInputManager的方法
- 注冊頁面可視狀態(tài)改變事件,并派發(fā)game.EVENT_HIDE和game.EVENT_SHOW事件。
來看看CCInputManager。
CCInputManager.js
// 注冊系統(tǒng)事件 element是canvas
registerSystemEvent (element) {
if(this._isRegisterEvent) return;
// 注冊過了,直接return
this._glView = cc.view;
let selfPointer = this;
let canvasBoundingRect = this._canvasBoundingRect;
// 監(jiān)聽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);
// 省略了鼠標(biāo)事件的注冊代碼
//_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;
// 計(jì)算偏移量
canvasBoundingRect.adjustedLeft = canvasBoundingRect.left - (body.scrollLeft || window.scrollX || 0);
canvasBoundingRect.adjustedTop = canvasBoundingRect.top - (body.scrollTop || window.scrollY || 0);
// 從事件中獲得觸摸點(diǎn),并調(diào)用回調(diào)函數(shù)
handler(selfPointer.getTouchesByEvent(event, canvasBoundingRect));
// 停止事件冒泡
event.stopPropagation();
event.preventDefault();
}), false);
};
for (let eventName in _touchEventsMap) {
registerTouchEvent(eventName);
}
}
// 修改屬性表示已完成事件注冊
this._isRegisterEvent = true;
}
在代碼中,主要完成的事情就是注冊了touchstart等一系列的原生事件,在事件回調(diào)中,則分別調(diào)用了selfPointer(=this)中的函數(shù)進(jìn)行處理。這里我們用touchstart事件作為例子,即handleTouchesBegin函數(shù)。
// 處理touchstart事件
handleTouchesBegin (touches) {
let selTouch, index, curTouch, touchID,
handleTouches = [], locTouchIntDict = this._touchesIntegerDict,
now = sys.now();
// 遍歷觸摸點(diǎn)
for (let i = 0, len = touches.length; i < len; i ++) {
// 當(dāng)前觸摸點(diǎn)
selTouch = touches[i];
// 觸摸點(diǎn)id
touchID = selTouch.getID();
// 觸摸點(diǎn)在觸摸點(diǎn)列表(this._touches)中的位置
index = locTouchIntDict[touchID];
// 如果沒有獲得index,說明是個(gè)新的觸摸點(diǎn)(剛按下去)
if (index == null) {
// 獲得一個(gè)沒有被使用的index
let unusedIndex = this._getUnUsedIndex();
// 取不到,拋出錯(cuò)誤??赡苁浅隽酥С值淖畲笥|摸點(diǎn)數(shù)量。
if (unusedIndex === -1) {
cc.logID(2300, unusedIndex);
continue;
}
//_curTouch = this._touches[unusedIndex] = selTouch;
// 存儲(chǔ)觸摸點(diǎn)
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;
// 加到需要處理的觸摸點(diǎn)列表中
handleTouches.push(curTouch);
}
}
// 如果有新觸點(diǎn),生成一個(gè)觸摸事件,分發(fā)到eventManager
if (handleTouches.length > 0) {
// 這個(gè)方法會(huì)把觸摸點(diǎn)的位置根據(jù)scale做處理
this._glView._convertTouchesWithScale(handleTouches);
let touchEvent = new cc.Event.EventTouch(handleTouches);
touchEvent._eventCode = cc.Event.EventTouch.BEGAN;
eventManager.dispatchEvent(touchEvent);
}
},
函數(shù)中,一部分代碼用于過濾是否有新的觸摸點(diǎn)產(chǎn)生,另一部分用于處理并分發(fā)事件(如果需要的話)。
到這里,事件就完成了從瀏覽器到引擎的轉(zhuǎn)化,事件已經(jīng)到達(dá)eventManager里。那么引擎到節(jié)點(diǎn)之間又經(jīng)歷了什么?
事件是怎么從引擎到節(jié)點(diǎn)的?
傳遞事件到節(jié)點(diǎn)的工作主要都發(fā)生在CCEventManager類中。包括了存儲(chǔ)事件監(jiān)聽器,分發(fā)事件等。先從_dispatchTouchEvent作為入口來看看。
CCEventManager.js
// 分發(fā)事件
_dispatchTouchEvent: function (event) {
// 為觸摸監(jiān)聽器排序
// TOUCH_ONE_BY_ONE:觸摸事件監(jiān)聽器類型,觸點(diǎn)會(huì)一個(gè)一個(gè)地分開被派發(fā)
// TOUCH_ALL_AT_ONCE:觸點(diǎn)會(huì)被一次性全部派發(fā)
this._sortEventListeners(ListenerID.TOUCH_ONE_BY_ONE);
this._sortEventListeners(ListenerID.TOUCH_ALL_AT_ONCE);
// 獲得監(jiān)聽器列表
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.
// 如果沒有任何監(jiān)聽器,直接return。
if (null === oneByOneListeners && null === allAtOnceListeners)
return;
// 存儲(chǔ)一下變量
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
// 不會(huì)翻。感覺是首先處理單個(gè)觸點(diǎn)的事件。
if (oneByOneListeners) {
// 遍歷觸點(diǎn),依次分發(fā)
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
// 不會(huì)翻。感覺是其次處理多觸點(diǎn)事件(一次性全部派發(fā))
if (allAtOnceListeners && mutableTouches.length > 0) {
this._dispatchEventToListeners(allAtOnceListeners, this._onTouchesEventCallback, {event: event, touches: mutableTouches});
if (event.isStopped())
return;
}
// 更新觸摸監(jiān)聽器列表,主要是移除和新增監(jiān)聽器
this._updateTouchListeners(event);
},
函數(shù)中,主要做的事情就是,排序、分發(fā)到注冊的監(jiān)聽器列表、更新監(jiān)聽器列表。平平無奇。你可能會(huì)奇怪,怎么有一個(gè)突兀的排序?哎,這正是重中之重!關(guān)于排序的作用,可以看官方文檔觸摸事件的傳遞。正是這個(gè)排序,實(shí)現(xiàn)了不同層級/不同zIndex的節(jié)點(diǎn)之間的觸點(diǎn)歸屬問題。排序會(huì)在后面提到,妙不可言。
分發(fā)事件是通過調(diào)用_dispatchEventToListeners函數(shù)實(shí)現(xiàn)的,接著就來看一下它的內(nèi)部實(shí)現(xiàn)。
/**
* 分發(fā)事件到監(jiān)聽器列表
* @param {*} listeners 監(jiān)聽器列表
* @param {*} onEvent 事件回調(diào)
* @param {*} eventOrArgs 事件/參數(shù)
*/
_dispatchEventToListeners: function (listeners, onEvent, eventOrArgs) {
// 是否需要停止繼續(xù)分發(fā)
var shouldStopPropagation = false;
// 獲得固定優(yōu)先級的監(jiān)聽器(系統(tǒng)事件)
var fixedPriorityListeners = listeners.getFixedPriorityListeners();
// 獲得場景圖優(yōu)先級別的監(jiān)聽器(我們添加的監(jiān)聽器正常都是在這里)
var sceneGraphPriorityListeners = listeners.getSceneGraphPriorityListeners();
/**
* 監(jiān)聽器觸發(fā)順序:
* 固定優(yōu)先級中優(yōu)先級 < 0
* 場景圖優(yōu)先級別
* 固定優(yōu)先級中優(yōu)先級 > 0
*/
var i = 0, j, selListener;
if (fixedPriorityListeners) { //_ priority < 0
if (fixedPriorityListeners.length !== 0) {
// 遍歷監(jiān)聽器分發(fā)事件
for (; i < listeners.gt0Index; ++i) {
selListener = fixedPriorityListeners[i];
// 若 監(jiān)聽器激活狀態(tài) 且 沒有被暫停 且 已被注冊到事件管理器
// 最后一個(gè)onEvent是使用_onTouchEventCallback函數(shù)分發(fā)事件到監(jiān)聽器
// onEvent會(huì)返回一個(gè)boolean,表示是否需要繼續(xù)向后續(xù)的監(jiān)聽器分發(fā)事件,若true,停止繼續(xù)分發(fā)
if (selListener.isEnabled() && !selListener._isPaused() && selListener._isRegistered() && onEvent(selListener, eventOrArgs)) {
shouldStopPropagation = true;
break;
}
}
}
}
// 省略另外兩個(gè)優(yōu)先級的觸發(fā)代碼
},
在函數(shù)中,通過遍歷監(jiān)聽器列表,將事件依次分發(fā)出去,并根據(jù)onEvent的返回值判定是否需要繼續(xù)派發(fā)。一般情況下,一個(gè)觸摸事件被節(jié)點(diǎn)接收到后,就會(huì)停止派發(fā)。隨后會(huì)從該節(jié)點(diǎn)進(jìn)行冒泡派發(fā)等邏輯。這也是一個(gè)重點(diǎn),即觸摸事件僅有一個(gè)節(jié)點(diǎn)會(huì)進(jìn)行響應(yīng),至于節(jié)點(diǎn)的優(yōu)先級,就是上面提到的排序算法啦。
這里的onEvent其實(shí)是_onTouchEventCallback函數(shù),來看看。
// 觸摸事件回調(diào)。分發(fā)事件到監(jiān)聽器
_onTouchEventCallback: function (listener, argsObj) {
//_ Skip if the listener was removed.
// 若 監(jiān)聽器已被移除,跳過。
if (!listener._isRegistered())
return false;
var event = argsObj.event, selTouch = event.currentTouch;
event.currentTarget = listener._node;
// isClaimed:監(jiān)聽器是否認(rèn)領(lǐng)事件
var isClaimed = false, removedIdx;
var getCode = event.getEventCode(), EventTouch = cc.Event.EventTouch;
// 若 事件為觸摸開始事件
if (getCode === EventTouch.BEGAN) {
// 若 不支持多點(diǎn)觸摸 且 當(dāng)前已經(jīng)有一個(gè)觸點(diǎn)了
if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch) {
// 若 該觸點(diǎn)已被節(jié)點(diǎn)認(rèn)領(lǐng) 且 該節(jié)點(diǎn)在節(jié)點(diǎn)樹中是激活的,則不處理事件
let node = eventManager._currentTouchListener._node;
if (node && node.activeInHierarchy) {
return false;
}
}
// 若 監(jiān)聽器有對應(yīng)事件
if (listener.onTouchBegan) {
// 嘗試分發(fā)給監(jiān)聽器,會(huì)返回一個(gè)boolean,表示監(jiān)聽器是否認(rèn)領(lǐng)該事件
isClaimed = listener.onTouchBegan(selTouch, event);
// 若 事件被認(rèn)領(lǐng) 且 監(jiān)聽器是已被注冊的,保存一些數(shù)據(jù)
if (isClaimed && listener._registered) {
listener._claimedTouches.push(selTouch);
eventManager._currentTouchListener = listener;
eventManager._currentTouch = selTouch;
}
}
}
// 若 監(jiān)聽器已有認(rèn)領(lǐng)的觸點(diǎn) 且 當(dāng)前觸點(diǎn)正是被當(dāng)前監(jiān)聽器認(rèn)領(lǐng)
else if (listener._claimedTouches.length > 0
&& ((removedIdx = listener._claimedTouches.indexOf(selTouch)) !== -1)) {
// 直接領(lǐng)回家
isClaimed = true;
// 若 不支持多點(diǎn)觸摸 且 已有觸點(diǎn) 且 已有觸點(diǎn)還不是當(dāng)前觸點(diǎn),不處理事件
if (!cc.macro.ENABLE_MULTI_TOUCH && eventManager._currentTouch && eventManager._currentTouch !== selTouch) {
return false;
}
// 分發(fā)事件給監(jiān)聽器
// ENDED或CANCELED的時(shí)候,需要清理監(jiān)聽器和事件管理器中的觸點(diǎn)
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.
// 若事件已經(jīng)被停止傳遞,直接return(對事件調(diào)用stopPropagationImmediate()等情況)
if (event.isStopped()) {
eventManager._updateTouchListeners(event);
return true;
}
// 若 事件被認(rèn)領(lǐng) 且 監(jiān)聽器把事件吃掉了(x)(指不需要再繼續(xù)傳遞,默認(rèn)為false,但在Node的touch系列事件中為true)
if (isClaimed && listener.swallowTouches) {
if (argsObj.needsMutableSet)
argsObj.touches.splice(selTouch, 1);
return true;
}
return false;
},
函數(shù)主要功能是分發(fā)事件,并對多觸點(diǎn)進(jìn)行兼容處理。重要的是返回值,當(dāng)事件被監(jiān)聽器認(rèn)領(lǐng)時(shí),就會(huì)返回true,阻止事件的繼續(xù)傳遞。
分發(fā)事件時(shí),以觸摸開始事件為例,會(huì)調(diào)用監(jiān)聽器的onTouchBegan方法。奇了怪了,不是分發(fā)給節(jié)點(diǎn)嘛?為什么是調(diào)用監(jiān)聽器?監(jiān)聽器是個(gè)什么東西?這就要研究一下,當(dāng)我們對節(jié)點(diǎn)調(diào)用on函數(shù)注冊事件的時(shí)候,事件注冊到了哪里?
事件是注冊到了哪里?
對節(jié)點(diǎn)調(diào)的on函數(shù),那相關(guān)代碼自然在CCNode里。直接來看看on函數(shù)都干了些啥。
/**
* 在節(jié)點(diǎn)上注冊指定類型的回調(diào)函數(shù)
* @param {*} type 事件類型
* @param {*} callback 回調(diào)函數(shù)
* @param {*} target 目標(biāo)(用于綁定this)
* @param {*} useCapture 注冊在捕獲階段
*/
on (type, callback, target, useCapture) {
// 是否是系統(tǒng)事件(鼠標(biāo)、觸摸)
let forDispatch = this._checknSetupSysEvent(type);
if (forDispatch) {
// 注冊事件
return this._onDispatch(type, callback, target, useCapture);
}
// 省略掉非系統(tǒng)事件的部分,其中包括了位置改變、尺寸改變等。
},
官方注釋老長一串,我給寫個(gè)簡化版??傊褪怯脕碜葬槍δ呈录幕卣{(diào)函數(shù)。
你可能想說,內(nèi)容這么少???然而這里分了兩個(gè)分支,一個(gè)是調(diào)用_checknSetupSysEvent函數(shù),一個(gè)是_onDispatch函數(shù),代碼都在里面555。
注冊相關(guān)的是_onDispatch函數(shù),另一個(gè)一會(huì)講。
// 注冊分發(fā)事件
_onDispatch (type, callback, target, useCapture) {
//_ Accept also patameters like: (type, callback, useCapture)
// 也可以接收這樣的參數(shù):(type, callback, useCapture)
// 參數(shù)兼容性處理
if (typeof target === 'boolean') {
useCapture = target;
target = undefined;
}
else useCapture = !!useCapture;
// 若 沒有回調(diào)函數(shù),報(bào)錯(cuò),return。
if (!callback) {
cc.errorID(6800);
return;
}
// 根據(jù)useCapture獲得不同的監(jiān)聽器。
var listeners = null;
if (useCapture) {
listeners = this._capturingListeners = this._capturingListeners || new EventTarget();
}
else {
listeners = this._bubblingListeners = this._bubblingListeners || new EventTarget();
}
// 若 已注冊了相同的回調(diào)事件,則不做處理
if ( !listeners.hasEventListener(type, callback, target) ) {
// 注冊事件到監(jiān)聽器
listeners.on(type, callback, target);
// 保存this到target的__eventTargets數(shù)組里,用于從target中調(diào)用targetOff函數(shù)來清除監(jiān)聽器。
if (target && target.__eventTargets) {
target.__eventTargets.push(this);
}
}
return callback;
},
節(jié)點(diǎn)會(huì)持有兩個(gè)監(jiān)聽器,一個(gè)是_capturingListeners,一個(gè)是_bubblingListeners,區(qū)別是什么呢?前者是注冊在捕獲階段的,后者是冒泡階段,更具體的區(qū)別后面會(huì)講。
從listeners.on(type, callback, target);可以看出其實(shí)事件是注冊在這兩個(gè)監(jiān)聽器中的,而不在節(jié)點(diǎn)里。
那就看看里面是個(gè)啥玩意。
event-target.js(EventTarget)
//_注冊事件目標(biāo)的特定事件類型回調(diào)。這種類型的事件應(yīng)該被 `emit` 觸發(fā)。
proto.on = function (type, callback, target, once) {
// 若 沒有傳遞回調(diào)函數(shù),報(bào)錯(cuò),return
if (!callback) {
cc.errorID(6800);
return;
}
// 若 已存在該回調(diào),不處理
if ( !this.hasEventListener(type, callback, target) ) {
// 注冊事件
this.__on(type, callback, target, once);
if (target && target.__eventTargets) {
target.__eventTargets.push(this);
}
}
return callback;
};
追到最后,又是一個(gè)on…由js.extend(EventTarget, CallbacksInvoker);可以看出,EventTarget繼承了CallbacksInvoker,再扒一層!
callbacks-invoker.js(CallbacksInvoker)
//_ 事件添加管理
proto.on = function (key, callback, target, once) {
// 獲得事件對應(yīng)的回調(diào)列表
let list = this._callbackTable[key];
// 若 不存在,到池子里取一個(gè)
if (!list) {
list = this._callbackTable[key] = callbackListPool.get();
}
// 把回調(diào)相關(guān)信息存起來
let info = callbackInfoPool.get();
info.set(callback, target, once);
list.callbackInfos.push(info);
};
終于到頭啦!其中,callbackListPool和callbackInfoPool都是js.Pool對象,這是一個(gè)對象池?;卣{(diào)函數(shù)最終會(huì)存儲(chǔ)在_callbackTable中。
了解完存儲(chǔ)的位置,那事件又是怎么被觸發(fā)的?
事件是怎么觸發(fā)的?
了解觸發(fā)之前,先來看看觸發(fā)順序。先看一段官方注釋。
鼠標(biāo)或觸摸事件會(huì)被系統(tǒng)調(diào)用 dispatchEvent 方法觸發(fā),觸發(fā)的過程包含三個(gè)階段:
* 1. 捕獲階段:派發(fā)事件給捕獲目標(biāo)(通過_getCapturingTargets獲?。?,比如,節(jié)點(diǎn)樹中注冊了捕獲階段的父節(jié)點(diǎn),從根節(jié)點(diǎn)開始派發(fā)直到目標(biāo)節(jié)點(diǎn)。
* 2. 目標(biāo)階段:派發(fā)給目標(biāo)節(jié)點(diǎn)的監(jiān)聽器。
* 3. 冒泡階段:派發(fā)事件給冒泡目標(biāo)(通過_getBubblingTargets獲?。热纾?jié)點(diǎn)樹中注冊了冒泡階段的父節(jié)點(diǎn),從目標(biāo)節(jié)點(diǎn)開始派發(fā)直到根節(jié)點(diǎn)。
啥意思呢?on函數(shù)的第四個(gè)參數(shù)useCapture,若為true,則事件會(huì)被注冊在捕獲階段,即可以最早被調(diào)用。
需要注意的是,捕獲階段的觸發(fā)順序是從父節(jié)點(diǎn)到子節(jié)點(diǎn)(從根節(jié)點(diǎn)開始)。隨后會(huì)觸發(fā)節(jié)點(diǎn)本身注冊的事件。最后,進(jìn)入冒泡階段,將事件從父節(jié)點(diǎn)傳遞到根節(jié)點(diǎn)。
簡單理解:捕獲階段從上到下,然后本身,最后冒泡階段從下到上。
理論可能有點(diǎn)生硬,一會(huì)看代碼就懂了!
還記得_checknSetupSysEvent函數(shù)嘛,前面的注釋只是寫了檢查是否為系統(tǒng)事件,其實(shí)它做的事情可不止這么一點(diǎn)點(diǎn)。
// 檢查是否是系統(tǒng)事件
_checknSetupSysEvent (type) {
// 是否需要新增監(jiān)聽器
let newAdded = false;
// 是否需要分發(fā)(系統(tǒng)事件需要)
let forDispatch = false;
// 若 事件是觸摸事件
if (_touchEvents.indexOf(type) !== -1) {
// 若 當(dāng)前沒有觸摸事件監(jiān)聽器 新建一個(gè)
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
});
// 將監(jiān)聽器添加到eventManager
eventManager.addListener(this._touchListener, this);
newAdded = true;
}
forDispatch = true;
}
// 省略事件是鼠標(biāo)事件的代碼,和觸摸事件差不多
// 若 新增了監(jiān)聽器 且 當(dāng)前節(jié)點(diǎn)不是活躍狀態(tài)
if (newAdded && !this._activeInHierarchy) {
// 稍后一小會(huì),若節(jié)點(diǎn)仍不是活躍狀態(tài),暫停節(jié)點(diǎn)的事件傳遞,
cc.director.getScheduler().schedule(function () {
if (!this._activeInHierarchy) {
eventManager.pauseTarget(this);
}
}, this, 0, 0, 0, false);
}
return forDispatch;
},
重點(diǎn)在哪呢?在eventManager.addListener(this._touchListener, this);這行??梢钥吹?,每個(gè)節(jié)點(diǎn)都會(huì)持有一個(gè)_touchListener,并將其添加到eventManager中。是不是有點(diǎn)眼熟?哎,這不就是剛剛eventManager分發(fā)事件時(shí)的玩意嘛!這不就連起來了嘛,雖然eventManager不持有節(jié)點(diǎn),但是持有這些監(jiān)聽器??!
新建監(jiān)聽器的時(shí)候,傳了一大堆參數(shù),還是拿熟悉的觸摸開始事件,onTouchBegan: _touchStartHandler,這又是個(gè)啥玩意呢?
// 觸摸開始事件處理器
var _touchStartHandler = function (touch, event) {
var pos = touch.getLocation();
var node = this.owner;
// 若 觸點(diǎn)在節(jié)點(diǎn)范圍內(nèi),則觸發(fā)事件,并返回true,表示這事件我領(lǐng)走啦!
if (node._hitTest(pos, this)) {
event.type = EventType.TOUCH_START;
event.touch = touch;
event.bubbles = true;
// 分發(fā)到本節(jié)點(diǎn)內(nèi)
node.dispatchEvent(event);
return true;
}
return false;
};
簡簡單單,獲得觸點(diǎn),判斷觸點(diǎn)是否落在節(jié)點(diǎn)內(nèi),是則分發(fā)!
//_ 分發(fā)事件到事件流中。
dispatchEvent (event) {
_doDispatchEvent(this, event);
_cachedArray.length = 0;
},
// 分發(fā)事件
function _doDispatchEvent (owner, event) {
var target, i;
event.target = owner;
//_ Event.CAPTURING_PHASE
// 捕獲階段
_cachedArray.length = 0;
// 獲得捕獲階段的節(jié)點(diǎn),儲(chǔ)存在_cachedArray
owner._getCapturingTargets(event.type, _cachedArray);
//_ capturing
event.eventPhase = 1;
// 從尾到頭遍歷(即從根節(jié)點(diǎn)到目標(biāo)節(jié)點(diǎn)的父節(jié)點(diǎn))
for (i = _cachedArray.length - 1; i >= 0; --i) {
target = _cachedArray[i];
// 若 目標(biāo)節(jié)點(diǎn)注冊了捕獲階段的監(jiān)聽器
if (target._capturingListeners) {
event.currentTarget = target;
//_ fire event
// 在目標(biāo)節(jié)點(diǎn)上處理事件
target._capturingListeners.emit(event.type, event, _cachedArray);
//_ check if propagation stopped
// 若 事件已經(jīng)停止傳遞了,return
if (event._propagationStopped) {
_cachedArray.length = 0;
return;
}
}
}
// 清空_cachedArray
_cachedArray.length = 0;
//_ Event.AT_TARGET
//_ checks if destroyed in capturing callbacks
// 目標(biāo)節(jié)點(diǎn)本身階段
event.eventPhase = 2;
event.currentTarget = owner;
// 若 自身注冊了捕獲階段的監(jiān)聽器,則處理事件
if (owner._capturingListeners) {
owner._capturingListeners.emit(event.type, event);
}
// 若 事件沒有被停止 且 自身注冊了冒泡階段的監(jiān)聽器,則處理事件
if (!event._propagationImmediateStopped && owner._bubblingListeners) {
owner._bubblingListeners.emit(event.type, event);
}
// 若 事件沒有被停止 且 事件需要冒泡處理(默認(rèn)true)
if (!event._propagationStopped && event.bubbles) {
//_ Event.BUBBLING_PHASE
// 冒泡階段
// 獲得冒泡階段的節(jié)點(diǎn)
owner._getBubblingTargets(event.type, _cachedArray);
//_ propagate
event.eventPhase = 3;
// 從頭到尾遍歷(實(shí)現(xiàn)從父節(jié)點(diǎn)到根節(jié)點(diǎn)),觸發(fā)邏輯和捕獲階段一致
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;
}
不知道看完有沒有對事件的觸發(fā)順序有更進(jìn)一步的了解呢?
其中對于捕獲階段的節(jié)點(diǎn)和冒泡階段的節(jié)點(diǎn),是通過別的函數(shù)來獲得的,用捕獲階段的代碼來做示例,兩者是類似的。
_getCapturingTargets (type, array) {
// 從父節(jié)點(diǎn)開始
var parent = this.parent;
// 若 父節(jié)點(diǎn)不為空(根節(jié)點(diǎn)的父節(jié)點(diǎn)為空)
while (parent) {
// 若 節(jié)點(diǎn)有捕獲階段的監(jiān)聽器 且 有對應(yīng)類型的監(jiān)聽事件,則把節(jié)點(diǎn)加到array數(shù)組中
if (parent._capturingListeners && parent._capturingListeners.hasEventListener(type)) {
array.push(parent);
}
// 設(shè)置節(jié)點(diǎn)為其父節(jié)點(diǎn)
parent = parent.parent;
}
},
一個(gè)自底向上的遍歷,將沿途符合條件的節(jié)點(diǎn)加到數(shù)組中,就得到了所有需要處理的節(jié)點(diǎn)!
好像有點(diǎn)偏題… 回到剛剛的事件分發(fā),同樣,因?yàn)椴还苁遣东@階段的監(jiān)聽器,還是冒泡階段的監(jiān)聽器,都是一個(gè)EventTarget,這邊拿自身的觸發(fā)來做示例。
owner._bubblingListeners.emit(event.type, event);
上面這行代碼將事件分發(fā)到自身節(jié)點(diǎn)的冒泡監(jiān)聽器里,所以直接看看emit里是什么。
emit其實(shí)是CallbacksInvoker里的方法。
callbacks-invoker.js
proto.emit = function (key, arg1, arg2, arg3, arg4, arg5) {
// 獲得事件列表
const list = this._callbackTable[key];
// 若 事件列表存在
if (list) {
// list.isInvoking 事件是否正在觸發(fā)
const rootInvoker = !list.isInvoking;
list.isInvoking = true;
// 獲得回調(diào)列表,遍歷
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;
// 若 回調(diào)函數(shù)是用once注冊的,那先把這個(gè)函數(shù)取消掉
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);
}
}
}
// 若 當(dāng)前事件沒有在被觸發(fā)
if (rootInvoker) {
list.isInvoking = false;
// 若 含有被取消的回調(diào),則調(diào)用purgeCanceled函數(shù),過濾已被移除的回調(diào)并壓縮數(shù)組
if (list.containCanceled) {
list.purgeCanceled();
}
}
}
};
核心是,根據(jù)事件獲得回調(diào)函數(shù)列表,遍歷調(diào)用,最后根據(jù)需要做一個(gè)回收。到此為止啦!
結(jié)尾
加點(diǎn)有意思的監(jiān)聽器排序算法
前面的內(nèi)容中,有提到_sortEventListeners函數(shù),用于將監(jiān)聽器按照觸發(fā)優(yōu)先級排序,這個(gè)算法我覺得蠻有趣的,與君共賞。
先理論。節(jié)點(diǎn)樹顧名思義肯定是個(gè)樹結(jié)構(gòu)。那如果樹中隨機(jī)取兩個(gè)節(jié)點(diǎn)A、B,有以下幾種種特殊情況:
- A和B屬于同一個(gè)父節(jié)點(diǎn)
- A和B不屬于同一個(gè)父節(jié)點(diǎn)
- A是B的某個(gè)父節(jié)點(diǎn)(反過來也一樣)
如果要排優(yōu)先級的話,應(yīng)該怎么排呢?令p1 p2分別等于A B。往上走:A = A.parent
- 最簡單的,直接比較_localZOrder
- A和B往上朔源,早晚會(huì)有一個(gè)共同的父節(jié)點(diǎn),這時(shí)如果比較_localZOrder,可能有點(diǎn)不公平,因?yàn)榭赡苡幸粋€(gè)節(jié)點(diǎn)走了很遠(yuǎn)的路(層級更高),應(yīng)該優(yōu)先觸發(fā)。此時(shí)又分情況:A和B層級一樣。那p1 p2往上走,走到相同父節(jié)點(diǎn),比較_localZOrder即可,A層級大于B。當(dāng)p走到根節(jié)點(diǎn)時(shí),將p交換到另一個(gè)起點(diǎn)。舉例:p2會(huì)先到達(dá)根節(jié)點(diǎn),此時(shí),把p2放到A位置,繼續(xù)。早晚他們會(huì)走過相同的距離,此時(shí)父節(jié)點(diǎn)相同。根據(jù)p1 p2的_localZOrder排序并取反即可。因?yàn)閷蛹壌蟮囊呀?jīng)被交換到另一邊了。這段要捋捋,妙不可言。
- 同樣往上朔源,但不一樣的是,因?yàn)橛懈缸雨P(guān)系,在交換走過相同距離后,p1 p2最終會(huì)在A或B節(jié)點(diǎn)相遇!所以此時(shí)只要判斷,是在A還是在B,若A,則A層級比較低,反之一樣。所以相遇的節(jié)點(diǎn)優(yōu)先級更低。
洋洋灑灑一大堆,上代碼,簡潔有力!
// 場景圖級優(yōu)先級監(jiān)聽器的排序算法
// 返回-1(負(fù)數(shù))表示l1優(yōu)先于l2,返回正數(shù)則相反,0表示相等
_sortEventListenersOfSceneGraphPriorityDes: function (l1, l2) {
// 獲得監(jiān)聽器所在的節(jié)點(diǎn)
let node1 = l1._getSceneGraphPriority(),
node2 = l2._getSceneGraphPriority();
// 若 監(jiān)聽器2為空 或 節(jié)點(diǎn)2為空 或 節(jié)點(diǎn)2不是活躍狀態(tài) 或 節(jié)點(diǎn)2是根節(jié)點(diǎn) 則l1優(yōu)先
if (!l2 || !node2 || !node2._activeInHierarchy || node2._parent === null)
return -1;
// 和上面的一樣
else if (!l1 || !node1 || !node1._activeInHierarchy || node1._parent === null)
return 1;
// 使用p1 p2暫存節(jié)點(diǎn)1 節(jié)點(diǎn)2
// ex:我推測是 是否發(fā)生交換的意思(exchange)
let p1 = node1, p2 = node2, ex = false;
// 若 p1 p2的父節(jié)不相等 則向上朔源
while (p1._parent._id !== p2._parent._id) {
// 若 p1的爺爺節(jié)點(diǎn)是空(p1的父節(jié)點(diǎn)是根節(jié)點(diǎn)) 則ex置為true,p1指向節(jié)點(diǎn)2。否則p1指向其父節(jié)點(diǎn)
p1 = p1._parent._parent === null ? (ex = true) && node2 : p1._parent;
p2 = p2._parent._parent === null ? (ex = true) && node1 : p2._parent;
}
// 若 p1和p2指向同一個(gè)節(jié)點(diǎn),即節(jié)點(diǎn)1、2存在某種父子關(guān)系,即情況3
if (p1._id === p2._id) {
// 若 p1指向節(jié)點(diǎn)2 則l1優(yōu)先。反之l2優(yōu)先
if (p1._id === node2._id)
return -1;
if (p1._id === node1._id)
return 1;
}
// 注:此時(shí)p1 p2的父節(jié)點(diǎn)相同
// 若ex為true 則節(jié)點(diǎn)1、2沒有父子關(guān)系,即情況2
// 若ex為false 則節(jié)點(diǎn)1、2父節(jié)點(diǎn)相同,即情況1
return ex ? p1._localZOrder - p2._localZOrder : p2._localZOrder - p1._localZOrder;
},
總結(jié)
游戲由CCGame而起,調(diào)用CCInputManager、CCEventManager注冊事件。隨后的交互里,由引擎的回調(diào)調(diào)用CCEventManager中的監(jiān)聽器們,再到CCNode中對于事件的處理。若命中,進(jìn)而傳遞到EventTarget中存儲(chǔ)的事件列表,便走完了這一路。
模塊其實(shí)沒有到很復(fù)雜的地步,但是涉及若干文件,加上各種兼容性、安全性處理,顯得多了起來。
以上就是詳解CocosCreator系統(tǒng)事件是怎么產(chǎn)生及觸發(fā)的的詳細(xì)內(nèi)容,更多關(guān)于CocosCreator系統(tǒng)事件產(chǎn)生及觸發(fā)的資料請關(guān)注腳本之家其它相關(guān)文章!
- CocosCreator入門教程之網(wǎng)絡(luò)通信
- Cocos2d-x 3.x入門教程(二):Node節(jié)點(diǎn)類
- Cocos2d-x 3.x入門教程(一):基礎(chǔ)概念
- Cocos2d-x入門教程(詳細(xì)的實(shí)例和講解)
- 詳解CocosCreator制作射擊游戲
- 如何在CocosCreator里畫個(gè)炫酷的雷達(dá)圖
- 詳解CocosCreator MVC架構(gòu)
- 詳解CocosCreator消息分發(fā)機(jī)制
- 如何用CocosCreator制作微信小游戲
- 怎樣在CocosCreator中使用游戲手柄
- 詳解CocosCreator華容道數(shù)字拼盤
- CocosCreator入門教程之用TS制作第一個(gè)游戲
相關(guān)文章
JS中錨點(diǎn)鏈接點(diǎn)擊平滑滾動(dòng)并自由調(diào)整到頂部位置
這篇文章主要介紹了JS中錨點(diǎn)鏈接點(diǎn)擊平滑滾動(dòng)并自由調(diào)整到頂部位置,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
p5.js臨摹動(dòng)態(tài)圖形實(shí)現(xiàn)方法詳解
這篇文章主要為大家詳細(xì)介紹了p5.js臨摹動(dòng)態(tài)圖形的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
js實(shí)現(xiàn)最短的XML格式化工具實(shí)例
這篇文章主要介紹了js實(shí)現(xiàn)最短的XML格式化工具,實(shí)例分析了基于jquery-latest.js實(shí)現(xiàn)XML代碼格式化的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-03-03
解決axios會(huì)發(fā)送兩次請求,有個(gè)OPTIONS請求的問題
這篇文章主要介紹了解決axios會(huì)發(fā)送兩次請求,有個(gè)OPTIONS請求的問題,需要的朋友可以參考下2018-10-10
基于JavaScript實(shí)現(xiàn)快速轉(zhuǎn)換文本語言(繁體中文和簡體中文)
這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)快速切換正體中文和簡體中文,需要的朋友可以參考下2016-03-03

