react源码-事件监听

栏目: IOS · Android · 发布时间: 6年前

内容简介:本文以 React v16.5.2 为基础进行源码分析在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁,至于下面的结构图形,则是对事件机制的一个图形化描述

本文以 React v16.5.2 为基础进行源码分析

基本流程

在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:

react源码-事件监听

事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁,至于下面的结构图形,则是对事件机制的一个图形化描述

  • React事件使用了事件委托的机制,一般事件委托的作用都是为了减少页面的注册事件数量,减少内存开销,优化浏览器性能,React这么做也是有这么一个目的,除此之外,也是为了能够更好的管理事件,实际上,React中所有的事件最后都是被委托到了 document这个顶级DOM上
  • 既然所有的事件都被委托到了 document上,那么肯定有一套管理机制,所有的事件都是以一种先进先出的队列方式进行触发与回调
  • 既然都已经接管事件了,那么不对事件做些额外的事情未免有些浪费,于是 React中就存在了自己的 合成事件(SyntheticEvent),合成事件由对应的 EventPlugin负责合成,不同类型的事件由不同的 plugin合成,例如 SimpleEvent Plugin、TapEvent Plugin等
  • 为了进一步提升事件的性能,使用了 EventPluginHub这个东西来负责合成事件对象的创建和销毁

#开始

<button onClick={this.autoFocus}>点击聚焦</button>
复制代码

这是我们在React中绑定事件的常规写法。经由JSX解析,button会被当做组件挂载。而onClick这时候也只是一个普通的props。 ReactDOMComponent在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):

事件注册

ReactDOMComponent.Mixin = {
    mountComponent:function(){},
    _createOpenTagMarkupAndPutListeners:function(){},
    ....,
    
    // 方法中有指向上次属性值得lastProp,
    // nextProp是当前属性值,这里nextProp是我们绑定给组件的onclick事件处理函数。
  //  nextProp 不为空调用enqueuePutListener绑定事件,为空则注销事件绑定。
     _updateDOMProperties:function(lastProps, nextProps, transaction){
         for (propKey in lastProps) {}   //省略。。。
         for (propKey in nextProps) {
         // 判断是否为事件属性
             if (registrationNameModules.hasOwnProperty(propKey))  {
          enqueuePutListener(this, propKey, nextProp, transaction);
              }
         }
         
     }
}


//这里进行事件绑定

首先判断了 rootContainerElement是不是一个 document或者 Fragment(文档片段节点)

enqueuePutListener 这个方法只在浏览器环境下执行,传给listenTo参数分别是事件名称'onclick'和代理事件的绑
     dom。如果是fragement 就是根节点(在reactDom.render指定的),不是的话就是documentlistenTo
     用于绑定事件到 document ,下面交由事务处理的是回调函数的存储,便于调用。
     ReactBrowserEventEmitter 文件中的 listenTo 看做事件处理的源头。
     这里获取了当前组件(其实这时候就是button)所在的document
     
     
function enqueuePutListener(inst, registrationName, listener, transaction) {
    ...
    var containerInfo = inst._hostContainerInfo;
    var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
    var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    
     
     
      listenTo(registrationName, doc);
      
     ...
}
复制代码

绑定的重点是这里的listenTo方法。看源码(ReactBrowerEventEmitter)

//registrationName:需要绑定的事件
//当前component所属的document,即事件需要绑定的位置

    listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
       //获取当前document上已经绑定的事件
    var isListening = getListeningForDocument(mountAt);
    // 获取 registrationName(注册事件名称)的topLevelEvent(顶级事件类型)
    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') {
           ...         
        } else if (dependency === 'topScroll') {
               ...
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
                ...
        } else if (topEventMapping.hasOwnProperty(dependency)) {
        // 获取 topLevelEvent 对应的浏览器原生事件
          //冒泡处理  
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }
        isListening[dependency] = true;
      }
    }
  },

复制代码

对于同一个事件,例如click有两个事件 onClick(在冒泡阶段触发) onClickCapture(在捕获阶段触发)两个事件名,这个冒泡和捕获都是react事件模拟出来的。绑定到 document上面的事件基本上都是在冒泡阶段(对 whell, focus, scroll 有额外处理),如下图 click 事件绑定执行的如下。

最后处理(EventListener的listen和capture中)

//eventType:事件类型,target: document对象,
//callback:是固定的,始终是ReactEventListener的dispatch方法
if (target.addEventListener) {
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    }
复制代码

所有事件绑定在document上

所以事件触发的都是ReactEventListener的dispatch方法

回调储存

看到这边你可能疑惑,所有回调都执行的ReactEventListener的dispatch方法,那我写的回调干嘛去了。别急,接着看:

function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  //注意这里!!!!!!!!!
  //这里获取了当前组件(其实这时候就是button)所在的document
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  //事件绑定
  listenTo(registrationName, doc);
 //这段代码表示将putListener放入回调序列,当组件挂载完成是会依次执行序列中的回调。putListener也是在那时候执行的。
 //不明白的可以看看本专栏中前两篇关于transaction和挂载机制的讲解
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
  //保存回调
  function putListener() {
    var listenerToPut = this;
    EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
  }
}
复制代码

还是这段代码,事件绑定我们介绍过,主要是listenTo方法。 当绑定完成以后会执行putListener。该方法会在ReactReconcileTransaction事务的close阶段执行,具体由EventPluginHub来进行管理

//
var listenerBank = {};
var getDictionaryKey = function (inst) {
//inst为组建的实例化对象
//_rootNodeID为组件的唯一标识
  return '.' + inst._rootNodeID;
}
var EventPluginHub = {
//inst为组建的实例化对象
//registrationName为事件名称
//listner为我们写的回调函数,也就是列子中的this.autoFocus
  putListener: function (inst, registrationName, listener) {
    ...
    var key = getDictionaryKey(inst);
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[key] = listener;
    ...
  }
}
复制代码

EventPluginHub在每个项目中只实例化一次。也就是说,项目组所有事件的回调都会储存在唯一的listenerBank中。

是不是有点晕,放上流程图,仔细回忆一下

react源码-事件监听

事件触发

注册事件时我们说过,所有的事件都是绑定在Document上。回调统一是ReactEventListener的dispatch方法。 由于冒泡机制,无论我们点击哪个DOM,最后都是由document响应(因为其他DOM根本没有事件监听)。也即是说都会触发dispatch

dispatchEvent: function(topLevelType, nativeEvent) {
    //实际触发事件的DOM对象
    var nativeEventTarget = getEventTarget(nativeEvent);
    //nativeEventTarget对应的virtual DOM
    var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
      nativeEventTarget,
    );
    ...
    //创建bookKeeping实例,为handleTopLevelImpl回调函数传递事件名和原生事件对象
    //其实就是把三个参数封装成一个对象
    var bookKeeping = getTopLevelCallbackBookKeeping(
      topLevelType,
      nativeEvent,
      targetInst,
    );

    try {
    //这里开启一个transactIon,perform中执行了
    //handleTopLevelImpl(bookKeeping)
      ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      releaseTopLevelCallbackBookKeeping(bookKeeping);
    }
  },
复制代码
function handleTopLevelImpl(bookKeeping) {
//触发事件的真实DOM
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  //nativeEventTarget对应的ReactElement
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  //bookKeeping.ancestors保存的是组件。
  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: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//首先封装event事件
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    //发送包装好的event
    runEventQueueInBatch(events);
  }
复制代码

事件封装

首先是EventPluginHub的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);
        ......
      }
    }
    return events;
  },
复制代码

接着看SimpleEventPlugin的方法

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    ......
    //这里是对事件的封装,但是不是我们关注的重点
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    //重点看这边
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
}
复制代码

接下来是方法中的各种引用,跳啊跳,转啊转,我们来到了ReactDOMTraversal中的traverseTwoPhase方法

//inst是触发事件的target的ReactElement
//fn:EventPropagator的accumulateDirectionalDispatches
//arg: 就是之前部分封装好的event(之所以说是部分,是因为现在也是在处理Event,这边处理完才是封装完成)
function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
   //注意path,这里以ReactElement的形式冒泡着,
   //把触发事件的父节点依次保存下来
    path.push(inst);
    //获取父节点
    inst = inst._hostParent;
  }
  var i;
  //捕捉,依次处理
  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);
  if (listener) {
  //如果有回调,就把包含该类型事件监听的DOM与对应的回调保存进Event。
  //accumulateInto可以理解成_.assign
  //记住这两个属性,很重要。
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}

复制代码

listenerAtPhase里面执行的是EventPluginHub的getListener函数

getListener: function (inst, registrationName) {
    //还记得之前保存回调的listenerBank吧?
    var bankForRegistrationName = listenerBank[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      return null;
    }
    //获取inst的_rootNodeId
    var key = getDictionaryKey(inst);
    //获取对应的回调
    return bankForRegistrationName && bankForRegistrationName[key];
  },
复制代码

事件分发

runEventQueueInBatch主要进行了两步操作

function runEventQueueInBatch(events) {
//将event事件加入processEventQueue序列
  EventPluginHub.enqueueEvents(events);
  //前一步保存好的processEventQueue依次执行
//executeDispatchesAndRelease
  EventPluginHub.processEventQueue(false);
}

  processEventQueue: function (simulated) {
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
    //重点看这里
    //forEachAccumulated可以看成forEach的封装
    //那么这里就是processingEventQueue保存的event依次执行executeDispatchesAndReleaseTopLevel(event)
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
  },
复制代码

executeDispatchesAndReleaseTopLevel(event)又是各种函数包装,最后干活的是

function executeDispatchesInOrder(event, simulated) {
  //对应的回调函数数组
  var dispatchListeners = event._dispatchListeners;
  //有eventType属性的ReactElement数组
  var dispatchInstances = event._dispatchInstances;
  
  ......
  
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}
复制代码

OK,这里总算出现了老熟人,在封装nativeEvent时我们保存在event里的两个属性,dispatchListeners与dispatchInstances,在这里起作用。 代码很简单,如果有处理这个事件的回调函数,就一次进行处理。细节我们稍后讨论,先看看这里是怎么处理的吧

function executeDispatch(event, simulated, listener, inst) {
//type是事件类型
  var type = event.type || 'unknown-event';
  //这是触发事件的真实DOM,也就是列子中的button
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
  //看这里看这里
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

复制代码

终于来到最后了,代码位于ReactErrorUtil中 (为了帮助开发,React通过模拟真正的浏览器事件来获得更好的devtools集成。这段代码在开发模式下运行)

//创造一个临时DOM
    var fakeNode = document.createElement('react');
    ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
    //绑定回调函数的上下文
      var boundFunc = func.bind(null, a);
      //定义事件类型
      var evtType = 'react-' + name;
      //绑定事件
      fakeNode.addEventListener(evtType, boundFunc, false);
      //生成原生事件
      var evt = document.createEvent('Event');
      //将原生事件处理成我们需要的类型
      evt.initEvent(evtType, false, false);
      //发布事件---这里会执行回调
      fakeNode.dispatchEvent(evt);
      //移出事件监听
      fakeNode.removeEventListener(evtType, boundFunc, false);
    };

复制代码

以上所述就是小编给大家介绍的《react源码-事件监听》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Computer Age Statistical Inference

Computer Age Statistical Inference

Bradley Efron、Trevor Hastie / Cambridge University Press / 2016-7-21 / USD 74.99

The twenty-first century has seen a breathtaking expansion of statistical methodology, both in scope and in influence. 'Big data', 'data science', and 'machine learning' have become familiar terms in ......一起来看看 《Computer Age Statistical Inference》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具