React的事件机制

      React Event的主要四个文件是 ReactBrowerEventEmitter.js(负责节点绑定的回调函数,该回调函数执行过程中构建合成事件对象,获取组件实例的绑定回调并执行,若有state变更,则重绘组件),ReactEventListener.js(负责事件注册和事件分发),ReactEventEmitter(负责事件的执行),EventPluginHub.js(负责事件的存储)和ReactEventEmitterMixin.js。
React的事件机制有2个特点:
      a. 使用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯直到document节点,因此React组件上声明的事件最终绑定到了document 上。由此减少了DOM操作,优化性能。
      b. 基于虚拟DOM实现SyntheticEvent合成事件

1.事件注册

在上一篇文章的提到onClick事件的例子,如下:

var LikeButton = React.createClass({
        getInitialState: function() {
          return {value: 0};
        },
        handleClick: function(event) {
          this.setState({value: this.state.value + 1});
          this.setState({value: this.state.value + 2});
        },
        render: function() {
          var text = '当前值为'+this.state.value;
          return (
          

{text}

); } });

      那么onClick事件是如何被注册到事件机制内部的呢?试想一下,在《React渲染机制》中我们已经知道,ReactDOM.render()会调用_renderSubtreeIntoContainer方法将新的Element添加到container中。_renderSubtreeIntoContainer方法首先会创建新Element 的虚拟DOM,在该虚拟DOM上进行操作。在将虚拟DOM转换组件的回调函数应该是在其创建(或更新的入口)作为该组件的属性存在的,在ReactDOMComponent.Mixin中,DOMComponent共享的创建和更新组件的入口方法mountComponent和updateComponent中,使用_updateDOMProperties方法来更新虚拟DOM属性。

_updateDOMProperties: function (lastProps, nextProps, transaction) {
    var propKey;
    var styleName;
    var styleUpdates;
     for (propKey in lastProps) { 
    if (registrationNameModules.hasOwnProperty(propKey)) { 
if (lastProps[propKey]) {
          deleteListener(this, propKey);
        }
      } 
    }
    for (propKey in nextProps) {
if (registrationNameModules.hasOwnProperty(propKey)) {
            if (nextProp) {
               enqueuePutListener(this, propKey, nextProp, transaction);
            } else if (lastProp) {
              deleteListener(this, propKey);
        }
      } 
  }

通过enqueuePutListener方法负责注册和存储事件

function enqueuePutListener(inst, registrationName, listener, transaction) {
  if (transaction instanceof ReactServerRenderingTransaction) {
    return;
  }
  if (process.env.NODE_ENV !== 'production') {
    // document 节点
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  //将事件注册到DOM节点
  listenTo(registrationName, doc);
// 事件存储
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
}

(1)listenTo() 负责注册事件,
可以发现React的事件机制框架
(2)采用事务队列的方式调用putListener将注册的事件存储起来,以供事件触发时回调。
      在此先对listenTo()注册事件进行分析,后面再分析负责事件存储的putListener。
      由listenTo = ReactBrowserEventEmitter.listenTo;在ReactBrowserEventEmitter
.js文件下找到listenTo方法,可以发现它主要解决了不同浏览器间捕获和冒泡不兼容的问题。事件回调方法在bubble阶段被触发。如果我们想让它在capture阶段触发,则需要在事件名上加capture。调用trapCapturedEvent和trapBubbledEvent来注册捕获和冒泡事件。

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

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
        if (dependency === 'topWheel') {
          if (isEventSupported('wheel')) {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);
          } else if (isEventSupported('mousewheel')) {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);
          } else {
            // Firefox needs to capture a different mouse scroll event.
            // @see http://www.quirksmode.org/dom/events/tests/scroll.html
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);
          }
        } else if (dependency === 'topScroll') {
          if (isEventSupported('scroll', true)) {
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);
          } else {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
          }
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
          if (isEventSupported('focus', true)) {
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);
          } else if (isEventSupported('focusin')) {
            // IE has `focusin` and `focusout` events which bubble.
            // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);
          }

          // to make sure blur and focus event listeners are only attached once
          isListening.topBlur = true;
          isListening.topFocus = true;
        } else if (topEventMapping.hasOwnProperty(dependency)) {
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }

        isListening[dependency] = true;
      }
    }
  }

2. 事件存储

      事件存储由EventPluginHub来负责,它的入口在我们上面讲到的enqueuePutListener中的putListener方法,如下

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

实际调用的是EventPluginHub.js中的putListener方法,EventPluginHub.js主要负责事件的存储、合成事件以对象池的方式实现创建和销毁。

putListener: function (inst, registrationName, listener) {
// key用来标识被注册事件react对象的NoodID。
var key = getDictionaryKey(inst);
//如果listenerBank存在registrationName事件元素取出该值,否则初始化。比如该实例中listenerBank[‘OnClick’]
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
//将该组件的注册的OnClick事件存入,即listenerBank[‘OnClick’][key] = listener;    bankForRegistrationName[key] = listener;
// EventPluginRegistry事件注册 插件,用于合成和分发事件,EventPluginRegistry.registrationNameModules[registrationName]用来提取’OnClick’的事件回调插件模型。
var PluginModule = 
EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  }

3.事件分发

      当事件触发时,注册在document上的回调函数会被触发。事件触发的入口函数是ReactEventListener.dispatchEvent负责分发已经注册的回调函数。在这个函数中会调用batchingStrategy 的 batchUpdate 方法实现批量处理更新。batchUpdate以transaction形式调用,批量处理更新。

@param  topLevelType 标识事件名
@param  nativeEvent 用户在浏览器上触发的原生事件
dispatchEvent: function (topLevelType, nativeEvent) {
    if (!ReactEventListener._enabled) {
      return;
    }
// 从对象池中提取事件对象
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
// 将React事件流放入批量处理队列中
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
// 回调执行结束后释放对象 
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

      ReactUpdates.batchedUpdates()的第一个参数是批量处理中的回调函数,后面的参数是传入回调函数的参数。
      TopLevelCallbackBookKeeping是一个类,该类对象用于记录topLevelType,nativeEvent和用于存储所有的祖先节点数组ancestors(当前是空的,只有分发时才会遍历并存储所有的祖先节点) 。
      那么传入batchedUpdates 内部的回调函数handleTopLevelImpl是什么呢???它其实就是事件分发的核心部分。

function handleTopLevelImpl(bookKeeping) {
// 获取原生的事件target
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 获取原生事件的target说在的组件,它是虚拟DOM
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  var ancestor = targetInst;
// 向上遍历所有的祖先节点并存到bookKeeping.ancestors 中。
//因为事件回调中可能会改变Virtual DOM结构,所以要先遍历好组件层级 
 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的实现方法在ReactBrowserEventEmitter.js里。

4.事件回调

      从上一节中知道各个节点分别调用ReactEventListener._handleTopLevel()方法来触发被注册的回调函数。
      _handleTopLevel 通过ReactDefaultInjection模块注入ReactEventListener模块,设为ReactBrowserEventEmitter.ReactEventListener属性 。ReactBrowserEventEmitter为ReactEventListener属性提供_handleTopLevel方法,即

ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);

handleTopLevel实际来自于ReactEventEmitterMixin模块的handleTopLevel:

handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//采用对象池的方式构造出合成事件。不同的eventType的合成事件可能不同
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 批处理队列中的events
    runEventQueueInBatch(events);
  }
4.1合成事件

      合成事件时一个跨浏览器原生事件包装器,具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,除了事件在所有浏览器中他们工作方式都相同。现在来看一看React是如何实现合成事件的。
EventPluginHub.js中extractEvents方法:

  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;
  }

      EventPluginHub.extractEvents()方法是通过执行各个插件的extractEvents方法来创建合成事件。possiblePlugin.extractEvents()根据事件的Type创建不同插件的合成事件,accumulateInto()负责将所有插件的合成事件存入到events数组中,形成当前事件的合成事件集合。EventPluginRegistry.plugins默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,分别是 SimpleEventPlugin、EnterLeaveEventPlugin、ChangeEventPlugin、SelectEventPlugin和BeforeInputEventPlugin。根据不同的事件类型采用不同的事件合成方法,这些事件合成方法有SyntheticAnimationEvent.js、SyntheticFocusEvent.js、SyntheticKeyboardEvent.js、SyntheticMouseEvent.js、SyntheticTouchEvent.js、SyntheticUIEvent.js、SyntheticWheelEvent.js等等一共13种合成方法。以SyntheticMouseEvent为例:

var MouseEventInterface = {
  screenX: null,
  screenY: null,
  clientX: null,
  clientY: null,
  ctrlKey: null,
  shiftKey: null,
  altKey: null,
  metaKey: null,
  getModifierState: getEventModifierState,
  button: function (event) {
    // Webkit, Firefox, IE9+
    // which:  1 2 3
    // button: 0 1 2 (standard)
    var button = event.button;
    if ('which' in event) {
      return button;
    }
    // IE<9
    // which:  undefined
    // button: 0 0 0
    // button: 1 4 2 (onmouseup)
    return button === 2 ? 2 : button === 4 ? 1 : 0;
  },
  buttons: null,
  relatedTarget: function (event) {
    return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);
  },
  // "Proprietary" Interface.
  pageX: function (event) {
    return 'pageX' in event ? event.pageX : event.clientX + ViewportMetrics.currentScrollLeft;
  },
  pageY: function (event) {
    return 'pageY' in event ? event.pageY : event.clientY + ViewportMetrics.currentScrollTop;
  }
};

function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
  return SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
}
module.exports = SyntheticMouseEvent;

      该方法主要考虑了mouseEvent在各个浏览器之间的兼容性问题。其返回值是SyntheticUIEvent执行的结果。SyntheticUIEvent.js中找到SyntheticUIEvent方法

function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
  return SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
}
SyntheticUIEvent是SyntheticEvent调用执行的结果,SyntheticEvent.js中主要是对合成事件的接口进行了封装,包括preventDefault()、stopPropagation()、persist()、isPersistent()、destructor()。

注意: 合成事件对象都是以pool方式创建和销毁的,这提高了React的性能,同时也意味着一旦事件执行结束该合成事件对象会被销毁。因此不能通过异步方式获取该事件。如下:

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // Won't work. this.state.clickEvent will only contain null values.
  this.setState({clickEvent: event});
  // You can still export event properties.
  this.setState({eventType: event.type});
}

如果想继续使用该事件可以使用e.persist()方法。

4.2事件执行

      事件合成之后就需要执行事件,React以批量处理事件队列的方式执行事件的。它的入口函数是在ReactEventEmitterMixin.js中的runEventQueueInBatch方法:

function runEventQueueInBatch(events) {
  EventPluginHub.enqueueEvents(events);
  EventPluginHub.processEventQueue(false);
}
EventPluginHub.enqueueEvents()方法是将合成事件写入队列,EventPluginHub.processEventQueue()方法是执行队列中的事件。
EventPluginHub.enqueueEvents()方法的实现逻辑很简单,使用accumulateInto方法将events存入eventQueue队列中。
  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  }
EventPluginHub.processEventQueue()方法的实现逻辑分为:

processEventQueue: function (simulated) {
// 现将eventQueue 设置为null,以便在处理过程中让新合成事件入队列
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
       // This would be a good time to rethrow if any of the event handlers threw.
    ReactErrorUtils.rethrowCaughtError();
  }

(1)获取合成事件队列,其中可能包含之前没处理完的合成事件
(2)遍历合成事件队列,

function forEachAccumulated(arr, cb, scope) {
  if (Array.isArray(arr)) {
    arr.forEach(cb, scope);
  } else if (arr) {
    cb.call(scope, arr);
  }
}

事件队列的回调函数为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);
    }
  }
};

React的事件系统主要分为3个步骤:
一是事件绑定,ReactBrowserEventEmitter的trapBubbledEvent等方法为节点或文档绑定事件;
二是事件监听,ReactEventListener.dispatchEvent将把该回调函数分发给事件对象的_dispatchListener属性;调用ReactBrowserEventEmitter.ReactEventListener方法以监听节点事件。
三是事件分发与触发,对触发的事件进行分发,并创建合成事件对象,在回调中用构建合成事件对象并执行合成事件对的象绑定回调。

你可能感兴趣的:(React的事件机制)