合成事件(SyntheticEvent)系统是React一个比较重要的部分,它兼容性很好,同时能够提升页面性能,那么合成事件系统在源码中是如何实现的呢?

从用户的交互触发一个事件后,主要要经过事件注册,分发,执行,销毁等过程。

事件绑定

比如我们在jsx中给一个按钮绑定一个onClick事件:

<button onClick = { this.onBtnClick.bind(this) } />

这段代码在React组件创建和更新的方法mountComponent和updateComponent中,会调用_updateDOMProperties方法来处理。

这里以ReactDOMComponent.js为例(位置:src/renderers/dom/stack/client/ReactDOMComponent.js):

可以看到这里的mountComponent中调用了_updateDOMProperties,它会对JSX中声明的组件属性进行处理,其中会调用到一个ensureListeningTo方法,这个方法很关键,负责注册JSX中声明的事件,来看源码(位置:src/renderers/dom/stack/client/ReactDOMComponent.js):

function ensureListeningTo(inst, registrationName, transaction) {
  if (transaction instanceof ReactServerRenderingTransaction) {
    return;
  }
  // 找到document
  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment =
    containerInfo._node &&
    containerInfo._node.nodeType === DOCUMENT_FRAGMENT_NODE;
  var doc = isDocumentFragment
    ? containerInfo._node
    : containerInfo._ownerDocument;

  // 将事件注册到document上  
  listenTo(registrationName, doc);
}

这里通过listenTo方法将事件注册到document这个原生dom节点上,该方法解决了不同浏览器间捕获和冒泡不兼容的问题,来看源码(位置 src/renderers/dom/shared/ReactBrowserEventEmitter.js):

listenTo: function(registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    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') {
          if (isEventSupported('wheel')) {
            // 使用trapBubbledEvent() 注册事件冒泡
            ReactDOMEventListener.trapBubbledEvent(
              'topWheel',
              'wheel',
              mountAt,
            );
          } else if (isEventSupported('mousewheel')) {
            ReactDOMEventListener.trapBubbledEvent(
              'topWheel',
              'mousewheel',
              mountAt,
            );
          } else {
            ReactDOMEventListener.trapBubbledEvent(
              'topWheel',
              'DOMMouseScroll',
              mountAt,
            );
          }
        } else if (dependency === 'topScroll') {
          // 使用trapBubbledEvent() 注册事件捕获
          ReactDOMEventListener.trapCapturedEvent(
            'topScroll',
            'scroll',
            mountAt,
          );
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
          ReactDOMEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);
          ReactDOMEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);

          isListening.topBlur = true;
          isListening.topFocus = true;
        } else if (dependency === 'topCancel') {
          if (isEventSupported('cancel', true)) {
            ReactDOMEventListener.trapCapturedEvent(
              'topCancel',
              'cancel',
              mountAt,
            );
          }
          isListening.topCancel = true;
        } else if (dependency === 'topClose') {
          if (isEventSupported('close', true)) {
            ReactDOMEventListener.trapCapturedEvent(
              'topClose',
              'close',
              mountAt,
            );
          }
          isListening.topClose = true;
        } else if (topLevelTypes.hasOwnProperty(dependency)) {
          ReactDOMEventListener.trapBubbledEvent(
            dependency,
            topLevelTypes[dependency],
            mountAt,
          );
        }

        isListening[dependency] = true;
      }
    }
  }

可以看到这里频繁的使用了ReactDOMEventListener.trapBubbledEvent()和ReactDOMEventListener.trapCapturedEvent()两个方法,它们分别是用来注册事件捕获和事件冒泡的,源码如下(位置 src/renderers/dom/shared/ReactDOMEventListener.js):

  /**
   * Traps top-level events by using event bubbling.
   */
  trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.listen(
      element,
      handlerBaseName,
      //用ReactDOMEventListener.dispatchEvent()进行事件分发,后面说明
      ReactDOMEventListener.dispatchEvent.bind(null, topLevelType),
    );
  },

  /**
   * Traps a top-level event by using event capturing.
   */
  trapCapturedEvent: function(topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.capture(
      element,
      handlerBaseName,
      //用ReactDOMEventListener.dispatchEvent()进行事件分发,后面说明
      ReactDOMEventListener.dispatchEvent.bind(null, topLevelType),
    );
  },

这里 trapBubbledEvent 返回的是EventListener.listen(),trapCapturedEvent 返回的是EventListener.capture(),这里的EventListener引用自fbjs/lib/EventListener模块,去(node_modules/fbjs/lib/EventListener.js)看下源码:

listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {

      //调用了addEventListener()
      target.addEventListener(eventType, callback, false);
      return {
        remove: function remove() {

          //调用了removeEventListener()
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

  /**
   * Listen to DOM events during the capture phase.
   */
  capture: function capture(target, eventType, callback) {
    if (target.addEventListener) {
      target.addEventListener(eventType, callback, true);
      return {
        remove: function remove() {
          target.removeEventListener(eventType, callback, true);
        }
      };
    } else {
      if (process.env.NODE_ENV !== 'production') {
        console.error('Attempted to listen to events during the capture phase on a ' + 'browser that does not support the capture phase. Your application ' + 'will not receive some events.');
      }
      return {
        remove: emptyFunction
      };
    }
  }

这里可以看到熟悉的原生js的addEventListener和removeEventListener方法,正是在这里将原生的js事件注册到页面的dom上的。

事件分发

上面代码里提到过ReactDOMEventListener.trapBubbledEvent()和ReactDOMEventListener.trapCapturedEvent()两个方法中都使用ReactDOMEventListener.dispatchEvent()进行事件分发,源码如下:

// nativeEvent是用户触发事件时,浏览器传递的原生事件(如click)
dispatchEvent: function(topLevelType, nativeEvent) {
    if (!ReactDOMEventListener._enabled) {
      return;
    }

    var nativeEventTarget = getEventTarget(nativeEvent);
    var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
      nativeEventTarget,
    );
    if (
      targetInst !== null &&
      typeof targetInst.tag === 'number' &&
      !ReactFiberTreeReflection.isFiberMounted(targetInst)
    ) {
      targetInst = null;
    }

    var bookKeeping = getTopLevelCallbackBookKeeping(
      topLevelType,
      nativeEvent,
      targetInst,
    );

    try {
   // 注意这里 ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      releaseTopLevelCallbackBookKeeping(bookKeeping);
    }
  }

dispatchEvent方法中将handleTopLevelImpl放入执行了ReactGenericBatching.batchedUpdates()进行批处理,实际的事件分发逻辑写在handleTopLevelImpl中:

function handleTopLevelImpl(bookKeeping) {
  // 找到事件触发的dom元素和React Component实例
  var targetInst = bookKeeping.targetInst;
  var ancestor = targetInst;
  do {
    if (!ancestor) {
      bookKeeping.ancestors.push(ancestor);
      break;
    }
    var root = findRootContainerNode(ancestor);
    if (!root) {
      break;
    }
    bookKeeping.ancestors.push(ancestor);
    ancestor = ReactDOMComponentTree.getClosestInstanceFromNode(root);
  } while (ancestor);

  //从当前组件向父组件遍历,依次执行注册的回调方法
  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    ReactDOMEventListener._handleTopLevel(
      bookKeeping.topLevelType,
      targetInst,
      bookKeeping.nativeEvent,
      getEventTarget(bookKeeping.nativeEvent),
    );
  }
}

上面代码让React自身实现了一套冒泡机制,遍历中执行的顺序就是冒泡的顺序,因此在React中不能通过stopPropagation来阻止冒泡。

事件系统还有不少复杂的逻辑,这里暂时不展开去看,有兴趣可以阅读源码研究。

results matching ""

    No results matching ""