事件注册
在组件加载(mountComponent)、更新(updateComponent)的时候都需要进行事件代码的注册。
我们跳过前面这些无所谓的 Call Stack,直接看一下enqueuePutListener
在项目开发过程中,遇到了一个具体的需求,需求要求点击 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注册和事件存储
上面我们有提到一个方法,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 之后又依次执行了
我们看一下 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上统一进行管理,大幅提升性能,且处理了不同浏览器的兼容性。
收工,告辞~

 
			愚墨
保持饥渴的专注,追求最佳的品质
文章评论