詳解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();
  });
}

其實核心代碼隻有一點點…為瞭保持對各個平臺的兼容性,
重要的地方有兩個:

  1. 調用CCInputManager的方法
  2. 註冊頁面可視狀態改變事件,並派發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,有以下幾種種特殊情況:

  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層級比較低,反之一樣。所以相遇的節點優先級更低。

洋洋灑灑一大堆,上代碼,簡潔有力!

// 場景圖級優先級監聽器的排序算法
// 返回-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系統事件產生及觸發的資料請關註WalkonNet其它相關文章!

推薦閱讀:

    None Found