zoukankan      html  css  js  c++  java
  • 源码看React 事件机制

    对React熟悉的同学都知道,React中的事件机制并不是原生的那一套,事件没有绑定在原生DOM上,发出的事件也是对原生事件的包装。
    那么这一切是怎么实现的呢?

    事件注册

    首先还是看我们熟悉的代码

    
    <button onClick={this.autoFocus}>点击聚焦</button>
    

    这是我们在React中绑定事件的常规写法。经由JSX解析,button会被当做组件挂载。而onClick这时候也只是一个普通的props。
    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) {
      ...
      //注意这里!!!!!!!!!
      //这里获取了当前组件(其实这时候就是button)所在的document
      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);
      }
    }
    

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

    
    //registrationName:需要绑定的事件
    //当前component所属的document,即事件需要绑定的位置
    listenTo: function (registrationName, contentDocumentHandle) {
        var mountAt = contentDocumentHandle;
        //获取当前document上已经绑定的事件
        var isListening = getListeningForDocument(mountAt);
        ...
          if (...) {
          //冒泡处理  
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(...);
          } else if (...) {
            //捕捉处理
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(...);
          }
          ...
      },
    

    最后处理(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中。

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

    事件触发

    注册事件时我们说过,所有的事件都是绑定在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);
      }
    

    事件封装

    首先是EventPluginHubextractEvents

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

    可以发现,React在分装原生nativeEvent时

    • 将有eventType属性的ReactElement放入 event._dispatchInstances
    • 将对应的回调依次放入event._dispatchListeners

    事件分发

    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里的两个属性,dispatchListenersdispatchInstances,在这里起作用。
    代码很简单,如果有处理这个事件的回调函数,就一次进行处理。细节我们稍后讨论,先看看这里是怎么处理的吧

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

    总体流程

    不难发现,我们经历了从真实DOM到Virtual DOM的来回转化。

    常见问题的答案。

    1. e.stopPropagation不能阻止原生事件冒泡
      event是封装好的事件。他是在document的回调里进行封装,并执行回调的。而原生的监听,在document接收到冒泡时早就执行完了。
    2. e.nativeEvent.stopPropagation,回调无法执行。
      很简单,因为冒泡是从里到外,执行了原生的阻止冒泡,document当如捕捉不到,document都没捕捉到,React还玩个球啊,要知道,一切操作都放在docuemnt的回调里了。
    3. 怎么避免两者影响

      
      这个答案大家说了很多次,避免原生事件与React事件混用,或者通过target进行判断。
      
      

    为什么这么设计

    在网上看过一个列子说得很好,
    一个Ul下面有1000个li标签。想在想为每个li都绑定一个事件,怎么操作?
    总不可能一个个绑定吧?
    其实这个和jquery绑定事件差不多。通过最外层绑定事件,当操作是点击任何一个li自然会冒泡到最外面的Ul,又可以通过最外面的target获取到具体操作的DOM。一次绑定,收益一群啊。

    来源:https://segmentfault.com/a/1190000011413181

  • 相关阅读:
    spring data jpa序列化问题
    观察者模式(bilibili)
    观察者模式之Spring: event和listener
    观察者模式之Observer和Subject
    Mybatis Plus官网
    Hackinglab之注入关
    利用GitHub进行团队开发
    IIS安装Web时数据库参数配置文件写入权限验证失败
    流量分析基础篇
    Mysql启动错误1045(28000)
  • 原文地址:https://www.cnblogs.com/datiangou/p/10161759.html
Copyright © 2011-2022 走看看