React 17 事件系统

1,095 阅读3分钟

1.背景

源码版本号为17.0.3,代码经常在变,可能你看到的跟我有所不同
react自己实现了一套事件委托机制,用合成事件替代了原生事件,模拟量冒泡捕获过程, 主要优点有 1.实现事件优先级。2.抹平浏览器差异
react17跟之前版本不同,把非委派事件绑定在root节点上,弱化了simpleEventPlugin的概念。。

2.实现

事件是怎么注册的?

1.对于需要委托的事件
jsx中用<div onClick={this.handle}></div>写事件绑定,实际上onClick方法会传入fiber的props中,并不像原生事件监听。
初始化时,在react-dom上下文中首先通过registerEvents方法把react事件名称跟对应的dependencies关系存到registrationNameDependencies中:维护一个数据结构 {'onclick':['click'],... }
并把所有的依赖存入allNativeEvents中,这里只会存一些需要委派的事件如click ,非委派的(主要是媒体事件)会绑在自己对应的el节点上,后文会讲到

function registerDirectEvent(registrationName, dependencies) {
  ...
  registrationNameDependencies[registrationName] = dependencies;
  {
    var lowerCasedName = registrationName.toLowerCase();
    possibleRegistrationNames[lowerCasedName] = registrationName;

    if (registrationName === 'onDoubleClick') {
      possibleRegistrationNames.ondblclick = registrationName;
    }
  }
  for (var i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}

在创建发fiberRoot根节点时createRootImpl调用listenToAllSupportedEvents,遍历所有allNativeEvents,根据事件类型执行方法listenToNativeEvent,这个方法实际上就调用了addTrappedEventListener方法,获取对应优先级的dispatchevent,用bind函数wrapper一下对这个listener绑定他的eventSystemFlags.最后通过我们熟悉的addEventlistener到root节点中去

function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) {
  var listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags); 
  var isPassiveListener = undefined;
  if (passiveBrowserEventsSupported) {
    if (domEventName === 'touchstart' || domEventName === 'touchmove' || domEventName === 'wheel') {
      isPassiveListener = true;
    }
  }
  targetContainer =  targetContainer;
  var unsubscribeListener; // When legacyFBSupport is enabled, it's for when we
  if (isCapturePhaseListener) {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventCaptureListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
    } else {
      unsubscribeListener = addEventCaptureListener(targetContainer, domEventName, listener);
    }
  } else {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventBubbleListenerWithPassiveFlag(targetContainer, domEventName, listener, isPassiveListener);
    } else {
      unsubscribeListener = addEventBubbleListener(targetContainer, domEventName, listener);
    }
  }
}

react-dom中对于每个原生事件都维护了对应的优先级。对于每个事件优先级的对应不同的dispatchEvent:『DiscreteEventPriority:dispatchDiscreteEvent,ContinuousEventPriority:dispatchContinuousEvent,DefaultEventPriority:dispatchEvent』,对于click使用的是DiscreteEventPriority,对应的lane为synclane=1

2.对于非委派事件主要有这些类型

var mediaEventTypes = ['abort', 'canplay', 'canplaythrough', 'durationchange', 'emptied', 'encrypted', 'ended', 'error', 'loadeddata', 'loadedmetadata', 'loadstart', 'pause', 'play', 'playing', 'progress', 'ratechange', 'seeked', 'seeking', 'stalled', 'suspend', 'timeupdate', 'volumechange', 'waiting']; 
var nonDelegatedEvents = new Set(['cancel', 'close', 'invalid', 'load', 'scroll', 'toggle'].concat(mediaEventTypes));

在completeWork阶段,对于hostComponent会执行finalizeInitialChildren方法
首先根据el的tag,即hostComponent fiber的type 执行listenToNonDelegatedEvent方法实现监听

function listenToNonDelegatedEvent(domEventName, targetElement) {
  {
    if (!nonDelegatedEvents.has(domEventName)) {
      error('Did not expect a listenToNonDelegatedEvent() call for "%s". ' + 'This is a bug in React. Please file an issue.', domEventName);
    }
  }
  var isCapturePhaseListener = false;
  var listenerSet = getEventListenerSet(targetElement);
  var listenerSetKey = getListenerSetKey(domEventName, isCapturePhaseListener);

  if (!listenerSet.has(listenerSetKey)) {
    addTrappedEventListener(targetElement, domEventName, IS_NON_DELEGATED, isCapturePhaseListener);
    listenerSet.add(listenerSetKey);
  }
}

逻辑就是对比维护的delegatedEvents列表 把事件名存入该el的listenerSet,后续逻辑跟上面差不多,根据事件优先级在冒泡或捕获阶段绑定对应的dispatchEvent事件

事件是怎么触发的?

以onClick举例,会调用之前的dispatchDiscreteEvent.bind() 以后的方法,里面的逻辑就是执行一遍未执行完的discreteEvent事件,然后调用dispatchEvent,dispatchevent里面也会刷一遍优先级更高的事件,然后走到如下方法。

function dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
 ...
  batchedEventUpdates(function () {
    return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
  });
}

batchedEventUpdates方法主要是打开批处理开关的,对于已经打开isBatchingEventUpdates开关的执行dispatchEventsForPlugins函数 对于未开的打开开关,先打开开关后执行dispatchEventsForPlugins再重置react的render时间跟用调度器刷一下队列,最后关上开关。用这种方式可以对短时间多次触发批处理。 dispatchEventsForPlugins作用是创建合成事件推入dispatchQueue队列,然后遍历执行

function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  var nativeEventTarget = getEventTarget(nativeEvent);
  var dispatchQueue = [];
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents主要逻辑是根据事件名找到对应的合成方法生成合成事件event,获取他是冒泡或捕获,执行accumulateSinglePhaseListeners方法,根据他是捕获事件还是冒泡事件获取不同的事件名称,遍历instance找对应props中名称一致的方法,积累到listeners上去
这会导致onClick onClickCapture 在两条listener上
listeners的数据结构:[{instance: FiberNode, currentTarget: div, listener: ƒ}]
大致代码:

function extractEvents$4(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) {
  var reactName = topLevelEventsToReactNames.get(domEventName);

  var SyntheticEventCtor = SyntheticEvent;
  var reactEventType = domEventName;

  switch (domEventName) {
    case 'keypress':
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent;
    case 'click':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    ...
  }

  var inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;

  {
    var accumulateTargetOnly = !inCapturePhase && // TODO: ideally, we'd eventually add all events from
    domEventName === 'scroll';
    var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly);
    if (_listeners.length > 0) {
      // Intentionally create event lazily.
      var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
      dispatchQueue.push({
        event: _event,
        listeners: _listeners
      });
    }
  }
}

然后执行processDispatchQueue,方法根据inCapturePhase模拟冒泡或者捕获,从左或右遍历listeners 遇到instance有event.isPropagationStopped()的停止执行,最后调用executeDispatch执行函数