React原始碼分析 – 事件機制

dzone發表於2019-02-20

React的事件機制還是很好玩的,其中模擬事件傳遞和利用document委託大部分事件的想法比較有意思。

事件機制流程圖

event-react

程式碼分析

(程式碼僅包含涉及事件引數的部分)

_updateDOMProperties是事件引數處理的入口,只要注意enqueuePutListener這個方法就好了,這是註冊事件的入口函式。registrationNameModules變數儲存事件型別和對應的方法的對映的一個物件,如圖:

registrationnamemodules

這些對映的初始化的地方在《React原始碼分析 – 元件初次渲染》解釋過了。

_updateDOMProperties: function (lastProps, nextProps, transaction) {
  var propKey;
  var styleName;
  var styleUpdates;
  for (propKey in lastProps) {
    if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
      continue;
    }
    if (registrationNameModules.hasOwnProperty(propKey)) {
      if (lastProps[propKey]) {
        // Only call deleteListener if there was a listener previously or
        // else willDeleteListener gets called when there wasn`t actually a
        // listener (e.g., onClick={null})
        deleteListener(this, propKey);
      }
    }
  }
  for (propKey in nextProps) {
    var nextProp = nextProps[propKey];
    var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined;
    if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
      continue;
    }
    if (registrationNameModules.hasOwnProperty(propKey)) { // 處理事件引數。
      if (nextProp) {
        enqueuePutListener(this, propKey, nextProp, transaction); // 註冊事件,委託到屬於的document上
      } else if (lastProp) {
        deleteListener(this, propKey);
      }
    }
  }
}
複製程式碼

enqueuePutListener

  • listenTo
  • putListener
function enqueuePutListener(inst, registrationName, listener, transaction) {
  var containerInfo = inst._nativeContainerInfo;
  var doc = containerInfo._ownerDocument; // 大部分的事件都被到對應的document上
  if (!doc) { // ssr
    // Server rendering.
    return;
  }
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
}
複製程式碼

listenTo是將事件委託到document的方法,大部分事件是委託到document上的。但是因為document上能夠catch的事件型別的限制(Document Object Model Events),不是所有的事件型別都委託到document,少部分是直接委託到元素本身上的。

putListener將對應的型別的事件、事件的目標物件和事件觸發時執行的方法新增到listenerBank物件中。

listenTo: function (registrationName, contentDocumentHandle) {
  var mountAt = contentDocumentHandle;
  var isListening = getListeningForDocument(mountAt);
  var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

  var topLevelTypes = EventConstants.topLevelTypes;
  for (var i = 0; i < dependencies.length; i++) {
    var dependency = dependencies[i];
    if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
      // 先判斷先幾個需要特殊處理的事件,主要都是相容性的原因。
      if (...) {
        ......
      } else if (topEventMapping.hasOwnProperty(dependency)) {
        ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
      }
      isListening[dependency] = true;
    }
  }
}

// 冒泡階段的觸發的事件的委託
trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
  return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
},

// 捕獲階段的觸發的事件的委託
trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
  return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
},

trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
  return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},

trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
  return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},

listen: function listen(target, eventType, callback) {
  if (target.addEventListener) {
    target.addEventListener(eventType, callback, false);
  }
},

capture: function capture(target, eventType, callback) {
  if (target.addEventListener) {
    target.addEventListener(eventType, callback, true);
  }
},
複製程式碼

重點在於所有的委託的事件的回撥函式都是ReactEventListener.dispatchEvent。

dispatchEvent: function (topLevelType, nativeEvent) {
  // bookKeeping的初始化使用了react在原始碼中用到的物件池的方法來避免多餘的垃圾回收。
  // bookKeeping的作用看ta的定義就知道了,就是一個用來儲存過程中會使用到的變數的物件。
  var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
  try {
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}
複製程式碼

handleTopLevelImpl方法遍歷事件觸發物件以及其的父級元素(事件傳遞),對每個元素執行_handleTopLevel方法。

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  var ancestor = targetInst;
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}
複製程式碼

handleTopLevel根據事件物件以及觸發的事件型別提取出所有需要被執行的事件以及對應的回撥函式,統一由runEventQueueInBatch執行。

handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
  runEventQueueInBatch(events);
}
複製程式碼

extractEvents方法呼叫了對應的plugin的extractEvents方法來獲取對應的plugin型別的需要執行的事件,然後accumulateInto到一起。

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
   var events;
   var plugins = EventPluginRegistry.plugins;
   for (var i = 0; i < plugins.length; i++) {
     // Not every plugin in the ordering may be loaded at runtime.
     var possiblePlugin = plugins[i];
     if (possiblePlugin) {
       var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
       if (extractedEvents) {
         events = accumulateInto(events, extractedEvents);
       }
     }
   }
   return events;
 }
複製程式碼

plugin的extractEvents方法中的有意思的地方在於 EventPropagators.accumulateTwoPhaseDispatches(event)

EventPropagators.accumulateTwoPhaseDispatches中模擬了事件傳遞的過程即:capture -> target -> bubble 的過程,將這個路徑上的所有的符合事件型別的回撥函式以及對應的元素按照事件傳遞的順序返回。

React原始碼分析 – 事件機制

(圖片來自Event dispatch and DOM event flow

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._nativeParent;
  }
  var i;
  for (i = path.length; i-- > 0;) {
    fn(path[i], false, arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], true, arg);
  }
}
複製程式碼

traverseTwoPhase方法模擬了事件傳遞的過程並且獲取對應的回撥函式和事件物件儲存在react合成的event物件的_dispatchListeners和_dispatchInstances上

function accumulateDirectionalDispatches(inst, upwards, event) {
  var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
  var listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    // event._dispatchListeners結果就是這個event在event flow的過程中會觸發那些listenter的callback【按照event flow的順序push到一個陣列中了】
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}
複製程式碼

查詢listener和對應的inst使用的是事件的型別以及_rootNodeID,listenerBank中儲存了對應一個型別下元素的回撥函式:

listenerBank

function listenerAtPhase(inst, event, propagationPhase) {
  var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
  return getListener(inst, registrationName);
}

getListener: function (inst, registrationName) {
    var bankForRegistrationName = listenerBank[registrationName];
    return bankForRegistrationName && bankForRegistrationName[inst._rootNodeID];
  },
複製程式碼

對於listenerBank內容的生成由之前說的第二個主要方法putListener完成。

putListener 使用事務的方式統一在ReactMountReady階段執行。

putListener: function (inst, registrationName, listener) {
  var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
  bankForRegistrationName[inst._rootNodeID] = listener;
}
複製程式碼

在extractEvents了對應觸發的事件型別的events後通過runEventQueueInBatch(events)將所有的合成事件放到事件佇列裡面,第二步是逐個執行

function runEventQueueInBatch(events) {
  EventPluginHub.enqueueEvents(events);
  EventPluginHub.processEventQueue(false);
}

function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  if (process.env.NODE_ENV !== `production`) {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || `unknown-event`;
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

function invokeGuardedCallback(name, func, a, b) {
  try {
    return func(a, b);
  } catch (x) {
    if (caughtError === null) {
      caughtError = x;
    }
    return undefined;
  }
}
複製程式碼

總結

  • 統一的分發函式dispatchEvent。
  • React的事件物件是合成物件(SyntheticEvent)。
  • 幾乎所有的事件都委託到document,達到效能優化的目的。
  • 合成事件與原生事件混用要注意React的事件基本都是委託到document。

參考資料

相關文章