react源码-事件监听

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

内容简介:本文以 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指定的),不是的话就是document。listenTo
     用于绑定事件到 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源码-事件监听》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

C#图解教程

C#图解教程

索利斯 / 苏林、朱晔 / 人民邮电出版社 / 2009-1 / 65.00元

本书是一本广受赞誉的C# 教程。它以图文并茂的形式,用朴实简洁的文字,并辅之以大量表格和代码示例,精炼而全面地阐述了最新版C# 语言的各种特性,使读者能够快速理解、学习和使用C#。同时, 本书还讲解了C#与VB 、C++ 等主流语言的不同点和相似之处。 本书是一本经典的C# 入门书,不仅适合没有任何编程语言基础的初级读者,而且还是有VB 、C++ 等语言基础的C# 初学者的最佳选择。一起来看看 《C#图解教程》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换