react事件系统

从源码讲解

react事件系统简单介绍

      react的virtual dom在内存中是以对象的形式存在的。react基于virtual dom实现了一个SyntheticEvent(合成事件)层,在定义的事件处理程序中,接收到的事件对象是一个SyntheticEvent对象的实例,该对象是对nativeEvent做了一层封装,完全符合w3c标准,并解决了各浏览器兼容性问题。

 

合成事件的绑定方式(jsx语法)

<button onClick={this.handleClick}> test </button>

原生DOM0级事件的绑定方式

<button onclick="handleClick()"> test </button>

react合成事件相对原生事件的特性

1. react底层,是采用事件委托的方式,它并没有把事件处理函数直接绑定到真实节点上,其实是绑定到html文档对象的根元素上(document.documentElement),这种方式对性能有很大的改善。

并且react对Synthetic对象进行利用,并不是每个事件处理程序都传入一个新的合成对象,而是同类型的事件复用一个Synthetic对象,只是修改里面相应的属性,这也节省了内存。

 

2. react合成事件相当于完全构造了一个新的事件对象,通过事件对象获取的属性,其实是获取Synthetic对象上的属性,而不是原生对象上的,通过重新事件对象解决了兼容性问题。

 

3.

react合成事件类型:(https://facebook.github.io/react/docs/events.html)

Mouse Events

Selection Events

Clipboard Events

Focus Events

....

 

 

源码讲解:

1. EventConstants.js  ,定义了一些常量,将原生所有的事件类型对应到react的事件系统中,‘click’ => 'topClick',但是写法还是onClick

 

2. react事件系统,通过多种插件系统组成

SimpleEventPlugin.js,通过判断不同的基本事件类型,创建不同的合成对象类型(SyntheticFocusEvent, SyntheticMouseEvent....)

 

react事件流程

3.ReactDomComponent.js

 _createOpenTagMarkupAndPutListeners 函数,解析react element上的属性

enqueuePutListener(this, 'onClick', function() {} , transaction) 函数,

提取事件类型的名字和事件处理程序进行注册

listenTo('onClick', documentElement),在html元素上绑定click事件

但是每次事件首先出发的是EventListener.dispatchEvent函数

 

原生事件系统,事件分为两个主要步骤:事件注册(在元素上绑定某个类型的事件),事件触发(通过交互动作,执行事件处理程序)

 

react自己内部实现了一个高效的事件注册、存储、分发执行、重用合成对象的事件系统,在原来的Dom事件体系上做了很大改进,减少了内存消耗,简化了事件逻辑,并最大化的解决了IE等浏览器的不兼容问题。与DOM事件体系相比,它有如下特点:

  1. 原生Dom事件,是在某个元素上绑定事件,并且一个元素上可以绑定:同类型事件多个事件处理程序

             不同类型事件多个事件处理程序

 而React组件上声明的事件,最终是绑定到了html文档的根元素上(document.documentElement),而不是React组件对应的dom节点。故只有document这个节点上面才绑定了DOM原生事件,其他节点没有绑定事件。这样简化了DOM原生事件,减少了内存开销。

并且在document节点上,对同类型的事件注册,只会注册一个(比如:在多个组件上注册了onClick事件,也只会在document节点上注册一个onClick事件)

 

 

 2. React有一套自己的合成事件SyntheticEvent,不同类型的事件会构造不同的SyntheticEvent

SyntheticMouseEvent
SyntheticSelectionEvent
SyntheticClipboardEvent
SyntheticFocusEvent

......

 

 3. React自身实现了一套事件捕获冒泡机制。

在事件触发的时候,提取合成事件,从触发事件的组件开始,向父级..上级遍历,此路径上的实例,反向遍历获得捕获阶段的listener,存放到_dispatchListeners上,此路径上的实例正向遍历获得冒泡阶段的listener存放到_dispatchListeners上。事件执行时,只要顺序执行_dispatchListeners里的函数。这样也就模拟实现了捕获冒泡机制。

 

 3. React使用对象池来管理合成事件对象的创建和销毁,这样减少了垃圾的生成和新对象内存的分配,大大提高了性能

事件注册

JSX中声明一个React事件十分简单,比如:

render() {
  return (
    <div onClick = {
            (event) => {console.log(JSON.stringify(event))}
        }
    />
  );
}

它是如何注册到react事件系统中的呢?

从组件创建和更新的入口方法mountComponent和updateComponent说起。在这两个方法中,都会调用到_updateDOMProperties方法,对JSX中声明的组件属性进行处理。源码如下:

   ReactDomComponent.js

  _updateDOMProperties: function (lastProps, nextProps, transaction) {
      ....
      } else if (registrationNameModules.hasOwnProperty(propKey)) {
     if (nextProp) {
     // enqueuePutListener注册事件
        enqueuePutListener(this, propKey, nextProp, transaction);
     } else if (lastProp) {
        deleteListener(this, propKey);
     }
  }

enqueuePutListener,它负责注册JSX中声明的事件。源码如下:

// inst: React Component对象
// registrationName: React合成事件名,如onClick
// listener: React事件回调方法,如onClick=callback中的callback
// transaction: mountComponent或updateComponent所处的事务流中,React都是基于事务流的
function enqueuePutListener(inst, registrationName, listener, transaction) {

  if (process.env.NODE_ENV !== 'production') {
    // IE8 has no API for event capturing and the `onScroll` event doesn't
    // bubble.
    process.env.NODE_ENV !== 'production' ? warning(registrationName !== 'onScroll' || 
    isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event') 
    : void 0;
  }
  var containerInfo = inst._nativeContainerInfo;
    //documentElement
  var doc = containerInfo._ownerDocument;
  if (!doc) {
    // Server rendering.
    return;
  }
  // 注册事件,将事件注册到document上
  listenTo(registrationName, doc);
  // 存储事件,放入事务队列中
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
}

enqueuePutListener主要做两件事,一方面将事件注册到document这个原生DOM上(这就是为什么只有document这个节点有DOM事件的原因),另一方面采用事务队列的方式调用putListener将注册的事件存储起来,以供事件触发时回调。

注册事件的入口是listenTo方法, 它解决了不同浏览器间捕获和冒泡不兼容的问题。事件回调方法在bubble阶段被触发。如果我们想让它在capture阶段触发,则需要在事件名上加上capture。比如onClick在bubble阶段触发,而onCaptureClick在capture阶段触发。listenTo代码虽然比较长,但逻辑很简单,调用ReactEventListener.js中trapCapturedEvent和trapBubbledEvent来注册捕获和冒泡事件。trapCapturedEvent大家可以自行分析,我们仅分析trapBubbledEvent,如下

  // topLevelType: react的顶层事件类型 ’topClick‘
  // handlerBaseName: 注册的事件类型 ’click‘
  // handle: 监听事件的元素,document根元素(名字有点别扭)
  trapBubbledEvent: function (topLevelType, handlerBaseName, handle) { 
    var element = handle;
    if (!element) {
      return null;
    }
    // 在html文档根元素上绑定事件,
    // listent(documentElement, 'click', function(){});
    // 底层就是调用addEventListener进行事件注册
    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },

事件回调函数的存储

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

 

ReactDomComponent.js

function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  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);
}
  /**
   * Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.
   * 利用事件名和react组件实例进行事件的存储
   */
  // inst: 组件实例
  // registrationName: 注册事件的名字 ’onClick‘
  // listener: 声明的事件处理程序 function
  putListener: function (inst, registrationName, listener) {
    console.blue('putListener.js');
    !(typeof listener === 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : invariant(false) : void 0;

    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[inst._rootNodeID] = listener;

    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

事件存储在了listenerBank对象中,它按照事件名和React组件对象进行了二维划分,比如Test组件上注册的onClick事件最后存储在listenerBank.onClick[Test._rootNodeId]中。

事件执行

事件分发

当事件触发时,document上addEventListener注册的callback会被回调。从前面事件注册部分发现,此时回调函数为ReactEventListener.dispatchEvent,它是事件分发的入口方法。下面我们来详细分析

  ReactEventListener.js

  trapBubbledEvent: function (topLevelType, handlerBaseName, handle) { 
    var element = handle;
    if (!element) {
      return null;
    }
    // dispatchEvent函数为每个事件处理程序的入口
    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
  },
  /*
   * 每个事件被触发的时候,该函数都会首先调用
  */
  // topLevelType: 'topClick'
  // nativeEvent: 原生事件对象
  dispatchEvent: function (topLevelType, nativeEvent) {
      console.red('topLevelType', topLevelType);
    if (!ReactEventListener._enabled) {
      return;
    }
    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
  }

handleTopLevelImpl才是事件分发的真正执行者,它是事件分发的核心,体现了React事件分发的特点,如下

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  // Loop through the hierarchy, in case there's any nested components.
  // It's important that we build the array of ancestors before calling any
  // event handlers, because event handlers can modify the DOM, leading to
  // inconsistencies with ReactMount's node cache. See #1105.
  var ancestor = targetInst;
  console.blue('bookKeeping.ancestors');
  do {
    bookKeeping.ancestors.push(ancestor);
    console.log('ancestor', 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));
  }
}

事件的调用

var ReactEventEmitterMixin = {

  /**
   * Streams a fired top-level event to `EventPluginHub` where plugins have the
   * opportunity to create `ReactEvent`s to be dispatched.
   */
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    // 利用原生事件,提取出对应的合成事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    // 批处理执行事件队列
    runEventQueueInBatch(events);
  }
};

事件处理由_handleTopLevel完成。它其实是调用ReactBrowserEventEmitter.handleTopLevel() ,如下

handleTopLevel方法是事件callback调用的核心。它主要做两件事情,一方面利用浏览器回传的原生事件构造出React合成事件,另一方面采用队列的方式处理events。先看如何构造合成事件。

构造合成事件

  /**
   * Allows registered plugins an opportunity to extract events from top-level
   * native browser events.
   *
   * @return {*} An accumulation of synthetic events.
   * @internal
   */
  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;
  },

 

EventPluginRegistry.plugins默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,且看代码

  /**
   * Some important event plugins included by default (without having to require
   * them).
   */
  ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
  });

不同的plugin针对不同的事件有特殊的处理,此处我们不展开讲了,下面仅分析SimpleEventPlugin中方法即可。

我们先看SimpleEventPlugin如何构造它所对应的React合成事件。

  // 根据不同事件类型,比如click,focus构造不同的合成事件SyntheticEvent, 如SyntheticKeyboardEvent SyntheticFocusEvent
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor;

   // 根据事件类型,采用不同的SyntheticEvent来构造不同的合成事件
    switch (topLevelType) {
      ... // 省略一些事件,我们仅以blur和focus为例
      case 'topBlur':
      case 'topFocus':
        EventConstructor = SyntheticFocusEvent;
        break;
      ... // 省略一些事件
    }

    // 从event对象池中取出合成事件对象,利用对象池思想,可以大大降低对象创建和销毁的时间,提高性能。这是React事件系统的一大亮点
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
},

这里采用了event对象池特性,采用合成事件对象池的方式,可以大大降低销毁和创建合成事件带来的性能开销。

合成对象创建完成后,这里react自己模拟实现了捕获和冒泡两个阶段,

在该合成对象上添加两个重要内部属性:

1._dispatchListeners:  从触发事件的组件开始,向上级组件遍历,来存储注册的捕获和冒泡的事件处理程序

2._dispatchInstances: 从触发事件的组件开始,向上级组件遍历,来存储注册事件处理程序的该组件实例

EventPropagator.js

function accumulateTwoPhaseDispatches(events) {
    console.blue('event eventPropagators', events);
    console.log('events', events);
  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}

对事件队列里的每个事件,执行accumulateTwoPhaseDispatchesSingle

function accumulateTwoPhaseDispatchesSingle(event) {
    console.log('event', event);
  if (event && event.dispatchConfig.phasedRegistrationNames) {
    EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
  }
}
 ReactDomTreeTraversal.js

/**
 * Simulates the traversal of a two-phase, capture/bubble event dispatch.
 */
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  // 从当前组件实例向上遍历寻找父级组件实例
  while (inst) {
    path.push(inst);
      console.log('inst', 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函数,模拟了捕获和冒泡两个阶段

traverseTwoPhase函数中的fn参数,就是EventPropagation.js中的accumulateDirectionalDispatches函数,用来向合成事件对象中累加对应组件实例的listener函数和相应实例

前面提到EventPluginHub.js putListener函数是用来存储listener,而获得相应组件实例上的注册的listener,用到了相应的getListener函数

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

批处理合成事件

react以队列的形式处理合成事件。方法入口为runEventQueueInBatch,如下

ReactEventEmitterMixin.js

function runEventQueueInBatch(events) {
    // 先将events事件放入队列中
    EventPluginHub.enqueueEvents(events);
    // 再处理队列中的事件,包括之前未处理完的。先入先处理原则
    EventPluginHub.processEventQueue(false);  
}
  processEventQueue: function (simulated) {
    // Set `eventQueue` to null before processing it so that we can tell if more
    // events get enqueued while processing.
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    !!eventQueue ? process.env.NODE_ENV !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.') : invariant(false) : void 0;
    // This would be a good time to rethrow if any of the event handlers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

合成事件处理也分为两步,先将我们要处理的events队列放入eventQueue中,因为之前可能就存在还没处理完的合成事件。然后再执行eventQueue中的事件。可见,如果之前有事件未处理完,这里就又有得到执行的机会了。

事件执行的入口方法为executeDispatchesAndReleaseTopLevel,如下

EventPluginHub.js

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


var executeDispatchesAndRelease = function (event, simulated) {
  if (event) {
    // 桉顺序执行合成对象上添加的事件处理程序
    EventPluginUtils.executeDispatchesInOrder(event, simulated);
    // 判断该合成对象是否持久化,没有持久化就释放该对象,以方便下次事件触发的时候重用该合成对象
    if (!event.isPersistent()) {
      event.constructor.release(event);
    }
  }
};
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
    console.blue('EventPluginUtils.js');
    console.log('event._dispatchListeners', dispatchListeners);
    console.log('event._dispatchInstances', 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);
    console.log('type', type);
    console.log('currentTarget2', event.currentTarget);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

executeDispatchesInOrder会先得到event对应的listeners队列,然后执行listeners数组上的每一个listener函数

ReactErrorUtils.js

//listener函数的调用
// 采用func(a)的方式进行调用,
// 故ReactErrorUtils.invokeGuardedCallback(type, listener, event)最终调用的是listener(event)
// event对象为浏览器传递的DOM原生事件对象,这也就解释了为什么React合成事件回调中能拿到原生event的原因
function invokeGuardedCallback(name, func, a) {
  try {
    func(a);
  } catch (x) {
    if (caughtError === null) {
      caughtError = x;
    }
  }
}

总结:

React事件系统还是相当麻烦的,主要分为事件注册,事件存储和事件执行三大部分。了解了React事件系统源码,就能够明白开头所列出的React事件几大特点了。

react事件系统源码讲解

By xuqinggang

react事件系统源码讲解

  • 395