環境
Cocos Creator 2.4
Chrome 88
概要
模塊作用
事件監聽機制應該是所有游戲都必不可少的內容。不管是按鈕的點擊還是物體的拖動,都少不了事件的監聽與分發。
主要的功能還是通過節點的on/once函數,對系統事件(如觸摸、點擊)進行監聽,隨后觸發對應的游戲邏輯。同時,也支持用戶發射/監聽自定義的事件,這方面可以看一下官方文檔監聽和發射事件。
涉及文件
其中,CCGame和CCInputManager都有涉及注冊事件,但他們負責的是不同的部分。
源碼解析
事件是怎么(從瀏覽器)到達引擎的?
想知道這個問題,必須要了解引擎和瀏覽器的交互是從何而起。
上代碼。
CCGame.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
// 初始化事件系統 _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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
// 注冊系統事件 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函數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 處理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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
// 分發事件 _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函數實現的,接著就來看一下它的內部實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
/** * 分發事件到監聽器列表 * @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函數,來看看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
// 觸摸事件回調。分發事件到監聽器 _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函數都干了些啥。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/** * 在節點上注冊指定類型的回調函數 * @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函數,另一個一會講。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 注冊分發事件 _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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//_注冊事件目標的特定事件類型回調。這種類型的事件應該被 `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)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//_ 事件添加管理 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函數嘛,前面的注釋只是寫了檢查是否為系統事件,其實它做的事情可不止這么一點點。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// 檢查是否是系統事件 _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
,這又是個啥玩意呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 觸摸開始事件處理器 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 ; }; |
簡簡單單,獲得觸點,判斷觸點是否落在節點內,是則分發!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
//_ 分發事件到事件流中。 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; } |
不知道看完有沒有對事件的觸發順序有更進一步的了解呢?
其中對于捕獲階段的節點和冒泡階段的節點,是通過別的函數來獲得的,用捕獲階段的代碼來做示例,兩者是類似的。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
_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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 場景圖級優先級監聽器的排序算法 // 返回-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系統事件產生及觸發的資料請關注服務器之家其它相關文章!
原文鏈接:https://blog.csdn.net/weixin_47879201/article/details/114675630