React源碼解讀系列 -- 事件機制

loveyou366 7年前發布 | 44K 次閱讀 React

React源碼解讀系列 – 事件機制

本文首先分析React在DOM事件上的架構設計、相關優化、合成事件(Synethic event)對象,從源碼層面上做到庖丁解牛的效果。同時,簡單介紹下react事件可能會遇到的問題。

1. 總體設計

react在事件處理上具有如下優點:

  • 幾乎所有的事件代理(delegate)到 document ,達到性能優化的目的
  • 對于每種類型的事件,擁有統一的分發函數 dispatchEvent
  • 事件對象(event)是合成對象(SyntheticEvent),不是原生的

react內部事件系統實現可以分為兩個階段: 事件注冊、事件觸發。

2. 事件注冊

ReactDOMComponent 在進行組件加載(mountComponent)、更新(updateComponent)的時候,需要對 props 進行處理(_updateDOMProperties):

ReactDOMComponent.Mixin = {
  _updateDOMProperties: function (lastProps, nextProps, transaction) {
    ...
    for (propKey in nextProps) {
      // 判斷是否為事件屬性
      if (registrationNameModules.hasOwnProperty(propKey)) {
        enqueuePutListener(this, propKey, nextProp, transaction);
      }
    }
  }
}
function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
  function putListener() {
    var listenerToPut = this;
    EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
  }
}

代碼解析:

  • 在props渲染的時候,如何屬性是事件屬性,則會用 enqueuePutListener 進行事件注冊
  • 上述 transaction 是ReactUpdates.ReactReconcileTransaction的實例化對象
  • enqueuePutListener進行兩件事情: 在 document 上注冊相關的事件;對事件進行存儲

2.1 document上事件注冊

document的事件注冊入口位于 ReactBrowserEventEmitter :

// ReactBrowserEventEmitter.js
listenTo: function (registrationName, contentDocumentHandle) {
  ...
  if (...) {
    ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
  } else if (...) {
    ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
  }
  ...
}

// ReactEventListener.js var ReactEventListener = { ... trapBubbledEvent: function (topLevelType, handlerBaseName, element) { ... var handler = ReactEventListener.dispatchEvent.bind(null, topLevelType); return EventListener.listen(element, handlerBaseName, handler); }, trapCapturedEvent: function (topLevelType, handlerBaseName, element) { var handler = ReactEventListener.dispatchEvent.bind(null, topLevelType); return EventListener.capture(element, handlerBaseName, handler); } dispatchEvent: function (topLevelType, nativeEvent) { ... ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); ... } } function handleTopLevelImpl(bookKeeping) { ... ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent)); ... }</code></pre>

代碼解析:

  • 事件的注冊、觸發,具體是在 ReactEventListener 中實現的
  • 事件的注冊有兩個方法: 支持冒泡(trapBubbledEvent)、trapCapturedEvent
  • document不管注冊的是什么事件,具有統一的回調函數 handleTopLevelImpl
  • document的回調函數中不包含任何的事物處理,只起到事件分發的作用

2.2 回調函數存儲

函數的存儲,在 ReactReconcileTransaction 事務的close階段執行:

transaction.getReactMountReady().enqueue(putListener, {
  inst: inst,
  registrationName: registrationName,
  listener: listener
});
function putListener() {
  var listenerToPut = this;
  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}

事件的存儲由 EventPluginHub 來進行管理,來看看其中的具體實現:

//
var listenerBank = {};
var getDictionaryKey = function (inst) {
  return '.' + inst._rootNodeID;
}
var EventPluginHub = {
  putListener: function (inst, registrationName, listener) {
    ...
    var key = getDictionaryKey(inst);
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[key] = listener;
    ...
  }
}

react中的所有事件的回調函數均存儲在 listenerBank 對象里面,根據事件類型、component對象的_rootNodeID為兩個key,來存儲對應的回調函數。

3. 事件的執行

事件注冊完之后,就可以依據事件委托進行事件的執行。由事件注冊可以知道,幾乎所有的事件均委托到document上,而document上事件的回調函數只有一個: ReactEventListener.dispatchEvent,然后進行相關的分發:

var ReactEventListener = {
  dispatchEvent: function (topLevelType, nativeEvent) {
    ...
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    ...
  }
}
function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

// 初始化時用ReactEventEmitterMixin注入進來的 ReactEventListener._handleTopLevel(..., nativeEventTarget, targetInst); } // ReactEventEmitterMixin.js var ReactEventEmitterMixin = { handleTopLevel: function (...) { var events = EventPluginHub.extractEvents(...); runEventQueueInBatch(events); } } function runEventQueueInBatch(events) { EventPluginHub.enqueueEvents(events); EventPluginHub.processEventQueue(false); }</code></pre>

代碼解析:

  • handleTopLevelImpl: 根據原生的事件對象,找到事件觸發的dom元素以及該dom對應的compoennt對象
  • ReactEventEmitterMixin: 一方面生成合成的事件對象,另一方面批量執行定義的回調函數
  • runEventQueueInBatch: 進行批量更新

3.1 合成事件的生成過程

react中的事件對象不是原生的事件對象,而是經過處理后的對象,下面從源碼層面解析是如何生成的:

// EventPluginHub.js
var EventPluginHub = {
  extractEvents: function (...) {
    var events;
    var plugins = EventPluginRegistry.plugins;
    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;
  }
}

EventPluginHub不僅存儲事件的回調函數,而且還管理其中不同的plugins,這些plugins是在系統啟動過程中注入(injection)過來的:

// react-dom模塊的入口文件ReactDOM.js:
var ReactDefaultInjection = require('./ReactDefaultInjection');
ReactDefaultInjection.inject();
...
// ReactDefaultInjection.js
module.exports = {
  inject: inject
};
function inject() {
  ...
  ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
  });
  ...
}

從上面代碼可以看到,默認情況下,react注入了五種事件plugin,針對不同的事件,得到不同的合成事件,以最常見的 SimpleEventPlugin 為例進行分析:

var SimpleEventPlugin = {
  extractEvents: function (topLevelType, ...) {
    var EventConstructor;
    switch (topLevelType) {
      EventConstructor = one of [ SyntheticEvent, SyntheticKeyboardEvent, SyntheticFocusEvent, SyntheticMouseEvent, SyntheticDragEvent, SyntheticTouchEvent, SyntheticAnimationEvent, SyntheticTransitionEvent, SyntheticUIEvent, SyntheticWheelEvent, SyntheticClipboardEvent];
    }
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
  }
}

代碼解析:

  • 針對不同的事件類型,會生成不同的合成事件
  • EventPropagators.accumulateTwoPhaseDispatches: 用于從EventPluginHub中獲取回調函數,后面小節會具體分析獲取過程

以其中的最基本的 SyntheticEvent 為例進行分析:

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

var Interface = this.constructor.Interface; for (var propName in Interface) { var normalize = Interface[propName]; if (normalize) { this[propName] = normalize(nativeEvent); } else { if (propName === 'target') { this.target = nativeEventTarget; } else { this[propName] = nativeEvent[propName]; } } } ... } _assign(SyntheticEvent.prototype, { preventDefault: function () { ... }, stopPropagation: function () { ... }, ... }); var EventInterface = { type: null, target: null, // currentTarget is set when dispatching; no use in copying it here currentTarget: emptyFunction.thatReturnsNull, eventPhase: null, bubbles: null, cancelable: null, timeStamp: function (event) { return event.timeStamp || Date.now(); }, defaultPrevented: null, isTrusted: null }; SyntheticEvent.Interface = EventInterface;

// 實現繼承關系 SyntheticEvent.augmentClass = function (Class, Interface) { ... }</code></pre>

3.2 獲取具體的回調函數

上述合成事件對象在生成的過程中,會從 EventPluginHub 處獲取相關的回調函數,具體實現如下:

// EventPropagators.js
function accumulateTwoPhaseDispatches(events) {
  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
function accumulateTwoPhaseDispatchesSingle(event) {
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
  }
}
function accumulateDirectionalDispatches(inst, phase, event) {
  var listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}
var getListener = EventPluginHub.getListener;
function listenerAtPhase(inst, event, propagationPhase) {
  var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
  return getListener(inst, registrationName);
}
// EventPluginHub.js
getListener: function (inst, registrationName) {
  var bankForRegistrationName = listenerBank[registrationName];
  var key = getDictionaryKey(inst);
  return bankForRegistrationName && bankForRegistrationName[key];
},

3.3 批量執行事件的具體回調函數

react會進行批量處理具體的回調函數,回調函數的執行為了兩步,第一步是將所有的合成事件放到事件隊列里面,第二步是逐個執行:

var eventQueue = null;
var EventPluginHub = {
  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },
  processEventQueue: function (simulated) {
    var processingEventQueue = eventQueue;
    ...
    forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    ...
  },
}
var executeDispatchesAndReleaseSimulated = function (e) {
  return executeDispatchesAndRelease(e, true);
};
var executeDispatchesAndRelease = function (event, simulated) {
  if (event) {
    EventPluginUtils.executeDispatchesInOrder(event, simulated);

if (!event.isPersistent()) {
  event.constructor.release(event);
}

} }; // EventPluginUtils.js function executeDispatchesInOrder(event, simulated) { var dispatchListeners = event._dispatchListeners; var dispatchInstances = event._dispatchInstances; ... executeDispatch(event, simulated, dispatchListeners, dispatchInstances); ... event._dispatchListeners = null; event._dispatchInstances = null; }</code></pre>

4. 可能存在的問題

4.1 合成事件與原生事件混用

在開發過程中,有時候需要使用到原生事件,例如存在如下的業務場景: 點擊input框展示日歷,點擊文檔其他部分,日歷消失,代碼如下:

// js部分
var React = require('react');
var ReactDOM = require('react-dom');
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showCalender: false
    };
  }
  componentDidMount() {
    document.addEventListener('click', () => {
      this.setState({showCalender: false});
      console.log('it is document')
    }, false);
  }
  render() {
    return (<div>
      <input
        type="text"
        onClick={(e) => {
          this.setState({showCalender: true});
          console.log('it is button')
          e.stopPropagation();
        }}
      />
      <Calendar isShow={this.state.showCalender}></Calendar>
    </div>);
  }
}

上述代碼: 在點擊input的時候,state狀態變成true,展示日歷,同時阻止冒泡,但是document上的click事件仍然觸發了?到底是什么原因造成的呢?

原因解讀: 因為react的事件基本都是委托到document上的,并沒有真正綁定到input元素上,所以在react中執行stopPropagation并沒有什么用處,document上的事件依然會觸發。

解決辦法:

4.1.1 input的onClick事件也使用原生事件

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showCalender: false
    };
  }
  componentDidMount() {
    document.addEventListener('click', () => {
      this.setState({showCalender: false});
      console.log('it is document')
    }, false);
    this.refs.myBtn.addEventListener('click', (e) => {
      this.setState({showCalender: true});
      e.stopPropagation();
    }, false);
  }
  render() {
    return (<div>
      <input
        type="text"
        ref="myBtn"
      />
      <Calendar isShow={this.state.showCalender}></Calendar>
    </div>);
  }
}

4.1.2 在document中進行判斷,排除目標元素

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showCalender: false
    };
  }
  componentDidMount() {
    document.addEventListener('click', (e) => {
      var tar = document.getElementById('myInput');
      if (tar.contains(e.target)) return;
      console.log('document!!!');
      this.setState({showCalender: false});
    }, false);
  }
  render() {
    return (<div>
      <input
        id="myInput"
        type="text"
        onClick={(e) => {
          this.setState({showCalender: true});
          console.log('it is button')
          // e.stopPropagation();
        }}
      />
      <Calendar isShow={this.state.showCalender}></Calendar>
    </div>);
  }
}

5. 小結

React在設計事件機制的時候,利用冒泡原理充分提高事件綁定的效率,使用 EventPluginHub 對回調函數、事件插件進行管理,然后通過一個統一的入口函數實現事件的分發,整個設計思考跟jQuery的事件實現上存在相似的地方,非常值得學習借鑒。

 

來自:http://zhenhua-lee.github.io/react/react-event.html

 

 本文由用戶 loveyou366 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!