愚墨的博客
  • 首页
  • 前端技术
  • 面试
只争朝夕不负韶华
  1. 首页
  2. 前端框架
  3. React
  4. 正文

React事件系统-源码解析

2020年01月04日 2777点热度 0人点赞 0条评论

起因

在项目开发过程中,遇到了一个具体的需求,需求要求点击 ant-d的tree组件节点的时候,单击触发一种效果,双击触发另一种效果。 因为antd 没此功能,所以需要自己去实现。 具体原理就是250毫秒内再次点击,则认定为双击,doubleClick。开发过程中遇到一个问题。

  clickNode = e => {
    ...
    console.log(e.target);  // 正常的target
    this.timeId = setTimeout(() => {
       console.log(e.target); // null
    }, 250);
  };

 

在setTimeout当中拿不到 e ,只有在回调的外部先存储想要的数据,内部才能访问。也就是说,在clickNode执行后,react 回收了e,想进一步了解react事件系统的执行,所以翻看了一下源码……

前提

因为项目当中已经已经使用了dll进行打包,所以无法直接 debugger react的源码,且我有懒得在node_modules外装,灵机一闪,想到了自己多年年前搞的 一个菜鸟级脚手架,刚好可以胜任本次行动,所以默默的打开了自己的github,附地址:https://github.com/rongchanghai/react-Learner

进行

脚手架中的react、react-dom 都是15.6.2,比较适合撸源码,并且当前react版本只是使用了Fiber重构了虚拟DOM的刷新机制,对事件系统没有什么影响。

我们都知道react没有采用原生JS的那套事件系统,而是自己实现了一套事件系统,这也是为什么我们在编码过程中,click 事件要写成 onClick 驼峰写法,而不是onclick的写法。并且react的事件系统是使用addEventLienster 将事件代理到document上。整体提升效率。

react事件系统总体分为两个部分

  • 事件注册
  • 事件触发

我们一步一步来看

ps:我们看源码不需要每行代码都看,有些可以直接略过,我们只关心主逻辑的代码。所以有一些我们就直接略过了。

整体的代码很简单

class Event extends React.Component {
  componentDidMount(){
    document.addEventListener('click', (e) => {
      console.log('document!!!');
    }, false);
  }
  innerClick(e) {
    console.log('A: react inner click.');
  }

  outerClick(e) {
    console.log('B: react outer click.');
  }
  render() {
    return (
      
); } }

事件注册

在组件加载(mountComponent)、更新(updateComponent)的时候都需要进行事件代码的注册。

我们跳过前面这些无所谓的 Call Stack,直接看一下enqueuePutListener

/**
 * @param {*} inst  -> ReactDOMComponent
 * @param {*} registrationName  -> "onClick"
 * @param {*} listener  -> cb
 * @param {*} transaction  -> ReactReconcileTransaction  react的事务调度器
 * @returns
 */
function enqueuePutListener(inst, registrationName, listener, transaction) {
    // 得到 document,因为要将所有事件注册到document上去
      var containerInfo = inst._hostContainerInfo;
      var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
      var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
    
    // 注册事件
    listenTo(registrationName, doc);
    // 
      transaction.getReactMountReady().enqueue(putListener, {
        inst: inst,
        registrationName: registrationName,
        listener: listener
      });
    }

在这段代码很简单,先获取到真实节点,判断节点是否是document,如果不是,则将其节点的顶层document以及事件名传入listenTo方法中进行注册。

listenTo

  function listenTo(registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    // dependencies  为 topClick
    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)) {
          //注册冒泡事件
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }

        isListening[dependency] = true;
      }
    }
  }

EventPluginRegistry.registrationNameDependencies 是一个map,所有的方法名,都会转成top开头的名称,不用管为什么。这只是react内部实现的一个叫法而已。

对于我们的这个topClick方法,核心的方法是 trapBubbledEvent。再深入看一下此方法。

trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.listen(
      element,
      handlerBaseName,
      //事件回调
      ReactEventListener.dispatchEvent.bind(null, topLevelType),
    );
  }

这个方法只是调用了 EventListener.listen 对事件进行绑定,再看一下此方法

var EventListener = {
/ **
*在冒泡阶段监听DOM事件。
*
* @param {DOMEventTarget}目标DOM元素,用于注册侦听器。
* @param {string} eventType事件类型,例如 “点击”或“鼠标悬停”。
* @param {function}回调回调函数。
* @return {object}具有`remove`方法的对象。
* /
listen: function listen(target, eventType, callback) {
        if (target.addEventListener) {
          target.addEventListener(eventType, callback, false);
          return {
            remove: function remove() {
              target.removeEventListener(eventType, callback, false);
            }
          };
        } else if (target.attachEvent) {
          target.attachEvent('on' + eventType, callback);
          return {
            remove: function remove() {
              target.detachEvent('on' + eventType, callback);
            }
          };
        }
      }

这个方法大家就能看懂了吧,就是一个正常的DOM事件绑定,做了一下兼容处理。(话说现在框架用多了,一些兼容方法都快忘了。再也不考虑IE 678了)。 但是我们的callback 比较特殊。不是我们普通的cb,而是一个加强版的。从trapBubbledEvent 中得知是 ReactEventListener.dispatchEvent.bind(null, topLevelType)。

dispatchEvent: function (topLevelType, nativeEvent) {
// nativeEvent 就是我们的原生event 对象,
    ...handleTopLevelImpl
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    ...
  }

上面的代码在事件触发阶段再讲。

我们会发现一个事情,就是说,我们绑定一个事件,我们在document上绑定的都是统一的回调函数handleTopLevelImpl,so?真正的事件回调在哪里呢???

listenTo 这条线完事了,退出来接着往下走。

function enqueuePutListener(inst, registrationName, listener, transaction) {
  ...
  listenTo(registrationName, doc);
  
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
CallbackQueue.prototype.enqueue = function enqueue(callback, context) {
    console.log(callback);
    this._callbacks = this._callbacks || [];
    this._callbacks.push(callback);
    this._contexts = this._contexts || [];
    this._contexts.push(context);
  };

 

这个简单就不解释了

加下来就是事件的存储操作。

注意我们用到了putListener 方法,putListener 中使用了EventPluginHub.putListener

var EventPluginHub = {
  ...
  / **
    * 将 listener 存储在“ listenerBank [registrationName] [key]”上。 来一波幂等。
    *
    * @param {object} inst实例,事件的来源。
    * @param {string} registrationName侦听器的名称(例如`onClick`)。
    * @param {function}监听器要存储的回调。
    * /
  putListener: function (inst, registrationName, listener) {
 
    // 这个key很重要。是我们的node Id
    var key = getDictionaryKey(inst);
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    bankForRegistrationName[key] = listener;
    // 以上,将DOM 和 cb 绑定到 放到 listenerBank中,建立起对应关系
    console.log('bankForRegistrationName', bankForRegistrationName);
    // 下面的代码是做的一个兼容,增加了一个空方法的绑定,解决safair上无法触发冒泡事件的问题。
    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  }

至此,事件注册阶段基本就完事了,我们回顾一下:

  • document上绑定事件
  • 依次调用 enqueuePutListener、listenTo、TrapBubbledEvent、EventListener.listen 进行绑定和处理兼容问题。将handleTopLevelImpl 事件绑定到document上。
  • 使用EventPluginHub.putListener 将我们的具体的DOM Id 和 具体的cb 进行对应绑定,放置到listenBank中

所以我们的事件注册阶段是分为两块的: document注册和事件存储

事件触发

上面我们有提到一个方法,handleTopLevelImpl,绑定的方法。现在可以看一下这个方法是干什么用的了

  function handleTopLevelImpl(bookKeeping) {
      console.log('事件触发');
      // 获取一下真实的DOM 节点,这里特殊点是 如果是文本的话,就返回其父节点,这里涉及到nodeType的知识,1、2、3等分别代元素、属性、文本,长久不用这个知识点都有点忘了。哈哈
      var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
      // 获得对应的ReactComponent
      var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

        //遍历层次结构,以防存在任何嵌套的组件。
        //重要的是我们在调用任何祖先之前先建立祖先数组
        //事件处理程序,因为事件处理程序可以修改DOM,从而导致
        //与ReactMount的节点缓存不一致。
        // 这波操作让我想起了redux中的一些逻辑。。。算了,想了解的看我之前的文章吧
      var ancestor = targetInst;
      do {
        bookKeeping.ancestors.push(ancestor);
        ancestor = ancestor && findParent(ancestor);
        console.log('ancestor', ancestor);
      } while (ancestor);
        // 循环执行cb,模拟冒泡操作
      for (var i = 0; i < bookKeeping.ancestors.length; i++) {
        targetInst = bookKeeping.ancestors[i];
        ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
      }
    }

在看 ReactEventListener._handleTopLevel 的代码

handleTopLevel: function handleTopLevel(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    debugger;
    // 合成事件对象
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    // 批量处理
    runEventQueueInBatch(events);
}
function runEventQueueInBatch(events) {
  EventPluginHub.enqueueEvents(events);
  EventPluginHub.processEventQueue(false);
}

且看这代码

extractEvents: function extractEvents(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);
            // 就是一个push 和 cancat的操作
            if (extractedEvents) {
              events = accumulateInto(events, extractedEvents);
            }
          }
        }
        // 返回一个事件对象数组
        return events;
 }

上述代码就是在合成事件的时候,会依次通过EventPluginRegistry.plugins插件列表来生成对应的事件数组,最后将这个生成的事件合并为一个数组返回。具体这些plugins,会根据不同的事件生成不同的事件对象。

对于不同的事件,React将使用不同的功能插件,这些插件都是通过依赖注入的方式进入内部使用的。React合成事件的过程非常繁琐,但可以概括出extractEvents函数内部主要是通过switch函数区分事件类型并调用不同的插件进行处理从而生成SyntheticEvent实例

接着看代码,

//触发该事件队列中的所有事件
  EventPluginHub.processEventQueue(false);
processEventQueue: function(simulated) {
    // 获取已合成事件队列
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseSimulated,
      );
    } else {
      // 让所有事件执行executeDispatchesAndReleaseTopLevel方法,
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseTopLevel,
      );
    }
    
    ReactErrorUtils.rethrowCaughtError();
}

在执行 forEachAccumulated 之后又依次执行了

image

我们看一下 executeDispatchesInOrder 方法

function executeDispatchesInOrder(event, simulated) {
      // debugger;
      // reactDom 和 回调函数
      var dispatchListeners = event._dispatchListeners;
      var dispatchInstances = event._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';
  // 获取实际dom
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    // 执行函数,
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
      var boundFunc = function boundFunc() {
      // 执行回调方法
        func(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);
};

到此,我们定义的回调函数就执行完了,但是要解决我最初的疑问,还得往下看。

  var executeDispatchesAndRelease = function executeDispatchesAndRelease(event, simulated) {
      if (event) {
        EventPluginUtils.executeDispatchesInOrder(event, simulated);

        if (!event.isPersistent()) {
        // 释放 event
          event.constructor.release(event);
        }
      }
    };
var standardReleaser = function standardReleaser(instance) {
      debugger;
      var Klass = this;
      !(instance instanceof Klass) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : _prodInvariant('25') : void 0;
      // 重要代码
      instance.destructor();
      if (Klass.instancePool.length < Klass.poolSize) {
        Klass.instancePool.push(instance);
      }
};

因为执行完成之后,react会将合成的时间对象进行释放,所以我们在setTimeout 之后异步拿到的event 其实已经被释放掉了,所以会报错。

综上,react 事件系统代码就看了一遍,其中有些我认为不是特别重的部分,就没有分析。整体是按照call stack的顺序来一步步分析。目的是解决最开始的疑问和了解事件系统的实现。

总结

其实react 之所以自己实现一波事件系统的主要目的还是为了提升执行效率,因为react不能控制用户coder的操作。如果有1000个DOM,都被绑定了各自的click事件,那执行起来效率就有点太低了。react 整体截获了所有的事件,然后将其挂载到document上统一进行管理,大幅提升性能,且处理了不同浏览器的兼容性。

收工,告辞~

标签: 暂无
最后更新:2020年11月30日

愚墨

保持饥渴的专注,追求最佳的品质

点赞
< 上一篇
下一篇 >

文章评论

取消回复

搜搜看看
历史遗迹
  • 2023年5月
  • 2022年9月
  • 2022年3月
  • 2022年2月
  • 2021年12月
  • 2021年8月
  • 2021年7月
  • 2021年5月
  • 2021年4月
  • 2021年2月
  • 2021年1月
  • 2020年12月
  • 2020年11月
  • 2020年9月
  • 2020年7月
  • 2020年5月
  • 2020年4月
  • 2020年3月
  • 2020年1月
  • 2019年5月
  • 2019年3月
  • 2019年2月
  • 2019年1月
  • 2018年9月
  • 2018年3月
  • 2018年2月
  • 2018年1月
  • 2017年11月
  • 2017年7月
  • 2017年6月
  • 2017年3月
  • 2017年2月
  • 2017年1月
  • 2016年12月
  • 2016年11月
  • 2016年9月
  • 2016年8月
  • 2016年7月
  • 2016年6月
  • 2016年5月
  • 2016年4月
  • 2016年3月
  • 2016年2月
  • 2016年1月
  • 2015年12月
  • 2015年10月
  • 2015年9月
  • 2015年7月
  • 2015年6月
  • 2015年4月

COPYRIGHT © 2020 愚墨的博客. ALL RIGHTS RESERVED.

THEME KRATOS MADE BY VTROIS