1. 程式人生 > >React事件機制-事件分發

React事件機制-事件分發

component native map unknown gist targe and pin tor

事件分發

之前講述了事件如何綁定在document上,那麽具體事件觸發的時候是如何分發到具體的監聽者呢?我們接著上次註冊的事件代理看。當我點擊update counter按鈕時,觸發註冊的click事件代理。

function dispatchInteractiveEvent(topLevelType, nativeEvent) {
  interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
function interactiveUpdates(fn, a, b) {
  return _interactiveUpdatesImpl(fn, a, b);
}
var _interactiveUpdatesImpl = function (fn, a, b) {
  return fn(a, b);
};

topLevelTypeclicknativeEvent為真實dom事件對象。看似很多,其實就做了一件事: 執行dispatchEvent(topLevelType, nativeEvent)。其實不然,_interactiveUpdatesImpl在後面被重新賦值為interactiveUpdates$1,完成了一次自我蛻變。

function setBatchingImplementation(batchedUpdatesImpl, interactiveUpdatesImpl, flushInteractiveUpdatesImpl) {
  _batchedUpdatesImpl = batchedUpdatesImpl;
  _interactiveUpdatesImpl = interactiveUpdatesImpl;
  _flushInteractiveUpdatesImpl = flushInteractiveUpdatesImpl;
}

function interactiveUpdates$1(fn, a, b) {
  if (!isBatchingUpdates && !isRendering && lowestPriorityPendingInteractiveExpirationTime !== NoWork) {
    performWork(lowestPriorityPendingInteractiveExpirationTime, false);
    lowestPriorityPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return scheduler.unstable_runWithPriority(scheduler.unstable_UserBlockingPriority, function () {
      return fn(a, b);
    });
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

setBatchingImplementation(batchedUpdates$1, interactiveUpdates$1, flushInteractiveUpdates$1);

如果有任何等待的交互更新,條件滿足的情況下會先同步更新,然後設置isBatchingUpdates,進行scheduler調度。最後同步更新。scheduler的各類優先級如下:

unstable_ImmediatePriority: 1
unstable_UserBlockingPriority: 2
unstable_NormalPriority: 3
unstable_LowPriority: 4
unstable_IdlePriority: 5

進入scheduler調度,根據優先級計算時間,開始執行傳入的回調函數。然後調用dispatchEvent,最後更新immediate work

flushImmediateWork裏的調用關系很復雜,最終會調用requestAnimationFrame進行更新,這裏不進行過多討論。

function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  var previousEventStartTime = currentEventStartTime;
  currentPriorityLevel = priorityLevel;
  currentEventStartTime = exports.unstable_now();

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentEventStartTime = previousEventStartTime;
    flushImmediateWork();
  }
}

下面看看dispatchEvent的具體執行過程。

function dispatchEvent(topLevelType, nativeEvent) {
  if (!_enabled) {
    return;
  }
  // 獲取事件觸發的原始節點
  var nativeEventTarget = getEventTarget(nativeEvent);
  // 獲取原始節點最近的fiber對象(通過緩存在dom上的internalInstanceKey屬性來尋找),如果沒找到會往父節點繼續尋找。
  var targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null && typeof targetInst.tag === 'number' && !isFiberMounted(targetInst)) {
    targetInst = null;
  }
  // 創建對象,包含事件名稱,原始事件,目標fiber對象和ancestor(空數組);如果緩存池有則直接取出並根據參數初始化屬性。
  var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst);

  try {
    // 批處理事件
    batchedUpdates(handleTopLevel, bookKeeping);
  } finally {
    // 釋放bookKeeping對象內存,並放入對象池緩存
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

接著看batchedUpdates,其實就是設置isBatching變量然後調用handleTopLevel(bookkeeping)

function batchedUpdates(fn, bookkeeping) {
  if (isBatching) {
    return fn(bookkeeping);
  }
  isBatching = true;
  try {
    // _batchedUpdatesImpl其實指向batchedUpdates$1函數,具體細節這裏不再贅述
    return _batchedUpdatesImpl(fn, bookkeeping);
  } finally {
    isBatching = false;
    var controlledComponentsHavePendingUpdates = needsStateRestore();
    if (controlledComponentsHavePendingUpdates) {
      _flushInteractiveUpdatesImpl();
      restoreStateIfNeeded();
    }
  }
}

所以將原始節點對應最近的fiber緩存在bookKeeping.ancestors中。

function handleTopLevel(bookKeeping) {
  var targetInst = bookKeeping.targetInst;
  var ancestor = targetInst;
  do {
    if (!ancestor) {
      bookKeeping.ancestors.push(ancestor);
      break;
    }
    var root = findRootContainerNode(ancestor);
    if (!root) {
      break;
    }
    bookKeeping.ancestors.push(ancestor);
    ancestor = getClosestInstanceFromNode(root);
  } while (ancestor);

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    runExtractedEventsInBatch(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

runExtractedEventsInBatch中調用了兩個方法: extractEventsrunEventsInBatch。前者構造合成事件,後者批處理合成事件。

function runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
  runEventsInBatch(events);
}

事件合成

 function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = null;

  for (var i = 0; i < plugins.length; i++) {
    var possiblePlugin = plugins[i];
    if (possiblePlugin) {
      var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);

      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }

  return events;
}

plugins是所有合成事件集合的數組,EventPluginHub初始化的時候完成註入。遍歷所有plugins,調用其extractEvents方法,返回構造的合成事件。accumulateInto函數則把合成事件放入events。本例click事件合適的pluginSimpleEventPlugin,其他plugin得到的extractedEvents都不滿足if (extractedEvents)條件。

EventPluginHubInjection.injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});

接下來看看構造合成事件的具體過程,這裏針對SimpleEventPlugin,其他plugin就不一一分析了,來看下其extractEvents:

extractEvents: function(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor = void 0;
    switch (topLevelType) {
      ...
      case TOP_CLICK:
      ...
        EventConstructor = SyntheticMouseEvent;
        break;
      ...    
    }
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    accumulateTwoPhaseDispatches(event);
    return event;
  }

topLevelEventsToDispatchConfig是一個map對象,存儲著各類事件對應的配置信息。這裏獲取到click的配置信息,然後根據topLevelType選擇對應的合成構造函數,這裏為SyntheticMouseEvent。接著從SyntheticMouseEvent合成事件對象池中獲取合成事件。調用EventConstructor.getPooled,最終調用的是getPooledEvent

註意: SyntheticEvent.extend方法中明確寫有addEventPoolingTo(Class);所以,SyntheticMouseEvent有eventPool、getPooled和release屬性。後面會詳細介紹SyntheticEvent.extend

function addEventPoolingTo(EventConstructor) {
  EventConstructor.eventPool = [];
  EventConstructor.getPooled = getPooledEvent;
  EventConstructor.release = releasePooledEvent;
}

首次觸發事件,對象池為空,所以這裏需要新創建。如果不為空,則取出一個並初始化。

function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  var EventConstructor = this;
  if (EventConstructor.eventPool.length) {
    var instance = EventConstructor.eventPool.pop();
    EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst);
    return instance;
  }
  return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst);
}

合成事件的屬性是由React主動生成的,一些屬性和原生事件的屬性名完全一致,使其完全符合W3C標準,因此在事件層面上具有跨瀏覽器兼容性。如果要訪問原生對象,通過nativeEvent屬性即可獲取。這裏SyntheticMouseEventSyntheticUIEvent擴展而來,而SyntheticUIEventSyntheticEvent擴展而來。

var SyntheticMouseEvent = SyntheticUIEvent.extend({
  ...
});

var SyntheticUIEvent = SyntheticEvent.extend({
  ...
});

SyntheticEvent.extend = function (Interface) {
  var Super = this;
  // 原型繼承
  var E = function () {};
  E.prototype = Super.prototype;
  var prototype = new E();
  // 構造繼承
  function Class() {
    return Super.apply(this, arguments);
  }
  _assign(prototype, Class.prototype);
  Class.prototype = prototype;
  Class.prototype.constructor = Class;

  Class.Interface = _assign({}, Super.Interface, Interface);
  Class.extend = Super.extend;
  addEventPoolingTo(Class);

  return Class;
};

當被new創建時,會調用父類SyntheticEvent進行構造。主要是將原生事件上的屬性掛載到合成事件上,還配置了一些額外屬性。

function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {
  this.dispatchConfig = dispatchConfig;
  this._targetInst = targetInst;
  this.nativeEvent = nativeEvent;
  ...
}

合成事件構造完成後,調用accumulateTwoPhaseDispatches

function accumulateTwoPhaseDispatches(events) {
  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}

// 循環處理所有的合成事件
function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

// 檢測事件是否具有捕獲階段和冒泡階段
function accumulateTwoPhaseDispatchesSingle(event) {
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
  }
}

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  // 循環遍歷當前元素及父元素,緩存至path
  while (inst) {
    path.push(inst);
    inst = getParent(inst);
  }
  var i = void 0;
  // 捕獲階段
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  // 冒泡階段
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

function accumulateDirectionalDispatches(inst, phase, event) {
  // 獲取當前階段對應的事件處理函數
  var listener = listenerAtPhase(inst, event, phase);
  // 將相關listener和目標fiber掛載到event對應的屬性上
  if (listener) {
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

事件執行(批處理合成事件)

首先將events合並到事件隊列,之前沒有處理完畢的隊列也一同合並。如果新的事件隊列為空,則退出。反之開始循環處理事件隊列中每一個eventforEachAccumulated前面有提到過,這裏不再贅述。

function runEventsInBatch(events) {
  if (events !== null) {
    eventQueue = accumulateInto(eventQueue, events);
  }
  var processingEventQueue = eventQueue;
  eventQueue = null;

  if (!processingEventQueue) {
    return;
  }

  forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
  rethrowCaughtError();
}

接下來看看事件處理,executeDispatchesAndRelease方法將事件執行和事件清理分開。

var executeDispatchesAndReleaseTopLevel = function (e) {
  return executeDispatchesAndRelease(e);
};

var executeDispatchesAndRelease = function (event) {
  if (event) {
    // 執行事件
    executeDispatchesInOrder(event);

    if (!event.isPersistent()) {
      // 事件清理,將合成事件放入對象池
      event.constructor.release(event);
    }
  }
};

提取事件的處理函數和對應的fiber,調用executeDispatch

function executeDispatchesInOrder(event) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

獲取真實dom掛載到event對象上,然後開始執行事件。

function executeDispatch(event, listener, inst) {
  var type = event.type || 'unknown-event';
  // 獲取真實dom
  event.currentTarget = getNodeFromInstance(inst);
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

invokeGuardedCallbackAndCatchFirstError下面調用的方法很多,最終會來到invokeGuardedCallbackImpl,關鍵就在func.apply(context, funcArgs);這裏的func就是listener(本例中是handleClick),而funcArgs就是合成事件對象。至此,事件執行完畢。

var invokeGuardedCallbackImpl = function (name, func, context, a, b, c, d, e, f) {
  var funcArgs = Array.prototype.slice.call(arguments, 3);
  try {
    func.apply(context, funcArgs);
  } catch (error) {
    this.onError(error);
  }
};

事件清理

事件執行完之後,剩下就是一些清理操作。event.constructor.release(event)相當於releasePooledEvent(event)。由於click對應的是SyntheticMouseEvent,所以會放入SyntheticMouseEvent.eventPool中。EVENT_POOL_SIZE固定為10。

function releasePooledEvent(event) {
  var EventConstructor = this;
  event.destructor();
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event);
  }
}

這裏做了兩件事,第一手動釋放event屬性上的內存(將屬性置為null),第二將event放入對象池。至此,清理工作完畢。

destructor: function () {
    ...
    this.dispatchConfig = null;
    this._targetInst = null;
    this.nativeEvent = null;
    this.isDefaultPrevented = functionThatReturnsFalse;
    this.isPropagationStopped = functionThatReturnsFalse;
    this._dispatchListeners = null;
    this._dispatchInstances = null;
    ...
}    

event清理完後,還會清理bookKeeping,同樣也會放入對象池進行緩存。同樣CALLBACK_BOOKKEEPING_POOL_SIZE也固定為10。

// callbackBookkeepingPool是react-dom中的全局變量
function releaseTopLevelCallbackBookKeeping(instance) {
  instance.topLevelType = null;
  instance.nativeEvent = null;
  instance.targetInst = null;
  instance.ancestors.length = 0;

  if (callbackBookkeepingPool.length < CALLBACK_BOOKKEEPING_POOL_SIZE) {
    callbackBookkeepingPool.push(instance);
  }
}

總結

最後執行performSyncWork。如果執行的事件內調用了this.setState,會進行reconciliationcommit。由於事件流的執行是批處理過程,同步調用this.setState不會立馬更新,需等待所有事件執行完成,即scheduler調度完後才開始performSyncWork,最終才能拿到新的state。如果是setTimeout或者是在dom上另外addEventListener的回調函數中調用this.setState則會立馬更新。因為執行回調函數的時候不經過React事件流。

React事件機制-事件分發