美文网首页
React 事件系统介绍及源码分析

React 事件系统介绍及源码分析

作者: 我家小八真可爱 | 来源:发表于2018-12-28 02:35 被阅读0次

    举个🌰

    大家在使用 React 的过程中应该都写过类似这样的代码:

    const list = [
      {
        id: 1,
        name: 'little white'
      },
      {
        id: 2,
        name: 'little white'
      }
    ];
    
    list.map(item => {
      return (
        <li key={item.id} onClick={this.handleClick}>
          {item.name}
        </li>
      );
    });
    

    如果 list 有 1000 项怎么办呢?

    React 事件系统

    几个关键概念

    概念 操作 优点
    事件委托 几乎将所有事件都委托到 document 减少内存占用和避免频繁的操作DOM
    合成事件 对原生 DOM 事件对象的封装 所有浏览器中都表现一致,实现了跨浏览器兼容
    对象池 利用对象池来管理合成事件对象的创建和销毁 便于统一管理;可以减少垃圾回收和新建对象过程中内存的分配操作,提高了性能
    • 对象池是什么? 对象池其实就是一个集合,里面包含了我们需要的对象集合,这些对象都被对象池所管理,如果需要这样的对象,从池子里取出来就行,但是用完需要归还。
    • 什么时候使用对象池? 初始化、实例化的代价高,且有需求需要经常实例化,但每次实例化的数量又比较少的情况下,使用对象池可以获得显著的效能提升。

    大体流程

    事件注册→事件触发→事件清理

    源码分析

    以开头的例子为示例代码进行分析。

    1.事件注册

    我们写在 li 元素属性上的事件监听函数最终是怎么绑定到原生 DOM上的?

    事件注册入口

    // 给最后要渲染的真实 DOM 对象设置属性
    function setInitialDOMProperties(tag, domElement, rootContainerElement, nextProps, isCustomComponentTag) {
      // propKey 为属性名
      const nextProp = nextProps[propKey];
      // registrationNameModules 是一个保存了与 React 事件相关的属性的对象
      // 判断该属性是否为与事件相关的属性
      else if (registrationNameModules.hasOwnProperty(propKey)) {
        if (nextProp != null) {
          ensureListeningTo(rootContainerElement, propKey);
        }
      }
    }
    

    确定事件最终注册到哪

    利用了事件委托,几乎所有的事件最终都会被委托到 document 或者 Fragment 上。

    function ensureListeningTo(rootContainerElement, registrationName) {
      // 判断当前根节点是不是 document 或者 Fragment
      const isDocumentOrFragment =
        rootContainerElement.nodeType === DOCUMENT_NODE ||
        rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
      // 不是的话就赋值为 document
      const doc = isDocumentOrFragment
        ? rootContainerElement
        : rootContainerElement.ownerDocument;
    
      listenTo(registrationName, doc);
    }
    

    获取原生 DOM 事件名和绑定事件的方式

    function listenTo(registrationName, mountAt) {
      // registrationNameDependencies 是一个存储了 React 事件名 与 原生事件名 映射的 Map
      // 获取原生 DOM 事件名,registrationName 为 onClick
      var dependencies = registrationNameDependencies[registrationName];
    
      // 遍历将原生 DOM 事件名数组,将事件依次进行绑定
      for (var i = 0; i < dependencies.length; i++) {
        var dependency = dependencies[i];
        if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
          switch (dependency) {
            ...
            // 除了不支持冒泡的事件外,大部分事件都会走default,调用 trapBubbledEvent
            default:
              var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
              if (!isMediaEvent) {
                trapBubbledEvent(dependency, mountAt);
              }
              break;
          }
          isListening[dependency] = true;
        }
      }
    }
    

    绑定冒泡阶段的事件监听函数

    function trapBubbledEvent(topLevelType, element) {
      // 获取 listener
      var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;
    
      addEventBubbleListener(
        element, // document
        getRawEventName(topLevelType),  // click
        dispatch.bind(null, topLevelType)
      );
    }
    
    // 利用原生的 addEventListener 将事件绑定到 document 上
    function addEventBubbleListener(element, eventType, listener) {
      element.addEventListener(eventType, listener, false);
    }
    

    2.事件触发

    点击 li 元素后,发生了什么,事件监听函数是如何被调用的?

    事件执行入口

    function dispatchEvent(topLevelType, nativeEvent) {
      try {
        // 批处理更新,这里实际上就是将当前触发的事件放入了 批处理队列 中
        batchedUpdates(handleTopLevel, bookKeeping);
      } finally {
        releaseTopLevelCallbackBookKeeping(bookKeeping);
      }
    }
    
    // handleTopLevel 最终会调用 runExtractedEventsInBatch
    function runExtractedEventsInBatch(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
      // 生成合成事件
      var events = extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
      // 批处理事件
      runEventsInBatch(events);
    }
    

    2.1 生成合成事件

    function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
      var events = null;
    
       // 不同类型的事件会采用不同的 plugin 来构造合成事件
      for (var i = 0; i < plugins.length; i++) {
        var possiblePlugin = plugins[i];
        if (possiblePlugin) {
          // 每个 plugin 都有一个 extractEvents 方法,用于生成特定事件类型的合成事件
          var extractedEvents = possiblePlugin.extractEvents(
            topLevelType,
            targetInst,
            nativeEvent,
            nativeEventTarget
          );
    
          if (extractedEvents) {
            events = accumulateInto(events, extractedEvents);
          }
        }
      }
    
      return events;
    }
    
    possiblePlugin.extractEvents = function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
      // 从 事件池 中去取一个合成事件对象
      var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
      // 模拟捕获和冒泡
      accumulateTwoPhaseDispatches(event);
    
      return event;
    }
    

    从对象池中取出合成事件

    React 事件系统的一大亮点,它将所有的合成事件都缓存在 对象池 中,可以大大降低对象的创建和销毁的时间,提升性能。

    function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
      var EventConstructor = this;
      // 如果对象池中有值,就直接取出
      if (EventConstructor.eventPool.length) {
        var instance = EventConstructor.eventPool.pop();
        EventConstructor.call(instance, dispatchConfig, targetInst, nativeEvent, nativeInst);
        return instance;
      }
      // 如果没有就新建一个
      return new EventConstructor(dispatchConfig, targetInst, nativeEvent, nativeInst);
    }
    

    模拟捕获和冒泡

    生成合成事件之后,会调用 accumulateTwoPhaseDispatches(event),该方法最终会调用 traverseTwoPhase

    function traverseTwoPhase(inst, fn, arg) {
      var path = [];
    
      // 查找祖先节点
      while (inst) {
        path.push(inst);
        inst = getParent(inst);
      }
    
      var i = void 0;
    
      // 反向遍历,由祖先节点到目标节点
      for (i = path.length; i-- > 0; ) {
        // fn 为 accumulateDirectionalDispatches
        fn(path[i], 'captured', arg);
      }
       // 正向遍历,由目标节点到祖先节点
      for (i = 0; i < path.length; i++) {
        fn(path[i], 'bubbled', arg);
      }
    }
    

    模拟过程中会把所有事件监听函数及其对应的节点都加入到 event(合成事件) 的属性中。

    function accumulateDirectionalDispatches(inst, phase, event) {
      // 取出当前阶段的事件监听器
      var listener = listenerAtPhase(inst, event, phase);
    
      if (listener) {
        event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
        event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
      }
    }
    

    2.2 执行事件

    function runEventsInBatch(events) {
      if (events !== null) {
        // 当前的合成事件加入到全局的事件队列 eventQueue 中
        eventQueue = accumulateInto(eventQueue, events);
      }
      // Set `eventQueue` to null before processing it so that we can tell if more
      // events get enqueued while processing.
      var processingEventQueue = eventQueue;
      eventQueue = null;
      // 对队列中的每个事件执行 executeDispatchesAndReleaseTopLevel 方法
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    
    var executeDispatchesAndReleaseTopLevel = function executeDispatchesAndReleaseTopLevel(e) {
      return executeDispatchesAndRelease(e);
    };
    
    

    按序执行和清理事件

    这也就是为什么当我们在需要异步读取操作一个合成事件对象的时候,需要执行 event.persist(),不然 React 就会在这里释放掉这个事件。

    var executeDispatchesAndRelease = function executeDispatchesAndRelease(event) {
      if (event) {
        executeDispatchesInOrder(event);
    
        if (!event.isPersistent()) {
          event.constructor.release(event);
        }
      }
    };
    

    回调函数真正被执行

    React 在收集回调数组的时候并不会去管我们是否调用了 stopPropagation ,而是会在事件执行的阶段才会去检查是否需要停止冒泡。

    function executeDispatchesInOrder(event) {
      // 取出合成事件的事件队列和节点数组
      var dispatchListeners = event._dispatchListeners;
      var dispatchInstances = event._dispatchInstances;
     
      // 依次遍历执行回调函数
      if (Array.isArray(dispatchListeners)) {
        for (var i = 0; i < dispatchListeners.length; i++) {
          // 判断是否需要阻止冒泡
          if (event.isPropagationStopped()) {
            break;
          } 
          // 执行回调函数
          executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
        }
      } else if (dispatchListeners) {
        executeDispatch(event, dispatchListeners, dispatchInstances);
      }
    
      // 执行完后清空这两个属性
      event._dispatchListeners = null;
      event._dispatchInstances = null;
    }
    

    3.事件清理

    事件执行完之后是如何清理的?

    //  事件执行完后会调用 event.constructor.release(event),实际调用的是 releasePooledEvent 方法
    var EVENT_POOL_SIZE = 10;
    
    function releasePooledEvent(event) {
      var EventConstructor = this;
      // 先释放合成事件对象的属性所占用的内存
      event.destructor();
      // 然后把清理后的 event 对象再放入对象池中,以便后续事件触发时进行二次利用
      if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
        EventConstructor.eventPool.push(event);
      }
    }
    

    再来举个🌰

    1. 当React 事件和原生事件混用时的阻止冒泡

    • React 事件系统 中调用 e.stopPropagation() 可以阻止 React 的合成事件以及绑定在 window 上的原生事件;
    • 原生事件 中调用 e.stopPropagation(),如果是在 document 之前,那么所有的 React 事件都会被阻止。
    class Example extends Component {
      // 组件加载完毕之后绑定原生事件
      componentDidMount() {
        document.getElementById('outer').addEventListener('click', e => {
          console.log('C: native outer click');
          // (1) e.stopPropagation();
        });
        document.addEventListener('click', () => console.log('D: native document click'));
        window.addEventListener('click', () => console.log('E: native window click'));
      }
    
      innerClick = e => {
        console.log('A: react inner click.');
        // (2) e.stopPropagation();
      };
    
      outerClick = () => {
        console.log('B: react outer click.');
      };
    
      render() {
        return (
          <div id="outer" onClick={this.outerClick}>
            <button id="inner" onClick={this.innerClick}>
              我是按钮
            </button>
          </div>
        );
      }
    }
    
    1. 点击 button 之后,输出结果是什么?(ABCDE排序)
    2. 分别把 (1)(2)e.stopPropagation() 加上,输出结果又是什么?(ABCDE排序)

    2.异步方式访问事件

    有一个模糊搜索框,为了避免频繁的发送请求,利用 setTimeout 加了 200 毫秒延时。

    <input
      onChange={e => {
        // 在异步调用之前让事件持久化,异步调用的时候才能获取到
        // e.persist();
        setTimeout(() => {
          this.handleSearch(e.target.value); // Cannot read property 'target' of undefined
        }, 200);
      }}
    />
    

    相关文章

      网友评论

          本文标题:React 事件系统介绍及源码分析

          本文链接:https://www.haomeiwen.com/subject/sufjlqtx.html