事件系统-随笔

发布时间 2023-04-30 23:18:13作者: 丢?的牧羊人

React 为我们提供了一套虚拟的事件系统,并且是遵循W3C规范来定义这些事件。在 React事件介绍 中介绍了合成事件对象以及为什么提供合成事件对象,主要原因是想抹平不同浏览器之间的兼容差异,提供一个可以跨平台的事件系统,并且能做优化和能干预事件的分发,为此就需要提供能在不同浏览器下一致的事件系统。
事件系统由事件注册、事件绑定、事件触发三个环节组成,事件注册负责注册事件形成映射关系,事件绑定在commit阶段会给原生节点上注册监听器,事件触发负责形成事件对象和收集事件,最终释放事件池。
⚠️: 章节内容对应的是React18版本,和旧版本可能存在差异,具体差异不会讲解。

事件注册

react 中,组件注册的事件,往往不是真正的事件,为什么怎么说,因为在react中注册事件的方式有两种,一种是原生事件,另外就是注册合成事件,并且合成事件最终也是注册在根容器上的,然后通过冒泡的形式最终触发。
在新的事件系统中,createRoot方法一次性往容器上注册所有支持的的事件(listenToAllSupportedEvents):

function createRoot (container, options) {
    ...省略
    listenToAllSupportedEvents(rootContainerElement);
}

listenToAllSupportedEvents就是注册事件的关键方法,来自packages/react-dom-bindings/src/events/DOMPluginEventSystem模块(react-dom-bindings下的所有模块都是和dom相关的),先是将事件插件全部加载进来(tu),然后才是执行listenToAllSupportedEvents方法

import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';

// TODO: remove top-level side effect.
SimpleEventPlugin.registerEvents();
EnterLeaveEventPlugin.registerEvents();
ChangeEventPlugin.registerEvents();
SelectEventPlugin.registerEvents();
BeforeInputEventPlugin.registerEvents();


// 省略部分.....


export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    allNativeEvents.forEach(domEventName => {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      if (domEventName !== 'selectionchange') {
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });

    const ownerDocument = (rootContainerElement: any).nodeType === DOCUMENT_NODE
        ? rootContainerElement
        : (rootContainerElement: any).ownerDocument;
    if (ownerDocument !== null) {
      // The selectionchange event also needs deduplication
      // but it is attached to the document.
      if (!(ownerDocument: any)[listeningMarker]) {
        (ownerDocument: any)[listeningMarker] = true;
        listenToNativeEvent('selectionchange', false, ownerDocument);
      }
    }
  }
}

注册完不同的事件插件后,就初始化好了一些全局变量。第一个是registrationNameDependencies,它包含的是React支持的事件类型,只有组件上的prop在这个对象内才会被当作事件处理。

{
    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    ...
} : Object

第二个是allNativeEvents,集合了所有定义的事件类型,listenToAllSupportedEvents方法中遍历的集合就是初始化后的allNativeEvents。

const allNativeEvents = ['click', 'change', ...]: Set

第三是topLevelEventsToReactNames,也是集合类型,实现了原生事件和合成事件的映射,事件触发阶段,会根据原生事件类型判断是否存在合成事件类型,才进行下一执行。

const topLevelEventsToReactNames = {
  "click" => "onClick"
  ......
}: Map

1. listenToAllSupportedEvents

事件插件加载完成全局变量的初始化,到这里就可以开始注册事件了。listenToAllSupportedEvents遍历事件类型集合allNativeEvents,将事件类型注册到容器上。

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
    // 判断根容器上是否有listeningMarker属性,有则说明已经事件已经注册过了
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;
    allNativeEvents.forEach(domEventName => {
         // selectionchange事件不注册在容器上
      if (domEventName !== 'selectionchange') {
           // ...重点
      }
    });

    const ownerDocument = (rootContainerElement: any).nodeType === DOCUMENT_NODE
        ? rootContainerElement
        : (rootContainerElement: any).ownerDocument;
    if (ownerDocument !== null) {
      // The selectionchange event also needs deduplication
      // but it is attached to the document.
      if (!(ownerDocument: any)[listeningMarker]) {
        (ownerDocument: any)[listeningMarker] = true;
        // 注册selectionchange事件
        listenToNativeEvent('selectionchange', false, ownerDocument);
      }
    }
  }
}

注册事件前会先判断是否已经注册过了,然后便利allNativeEvents集合,判断事件不是selectionchange才注册,当所有的事件注册结束后,才将selectionchange注册到ownerDocument上。

if (!nonDelegatedEvents.has(domEventName)) {
    listenToNativeEvent(domEventName, false, rootContainerElement);
}
    listenToNativeEvent(domEventName, true, rootContainerElement);

这里将事件名,容器和一个判断冒泡或捕获的布尔值到listentToNativeEvent。

export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget,
): void {
  
  // 省略...

  let eventSystemFlags = 0;
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }
  addTrappedEventListener(
    target,    // 容器
    domEventName,    // 事件名
    eventSystemFlags,    // 事件标示
    isCapturePhaseListener,    // 冒泡 | 捕获
  );
}

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
    let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );
    
    // 省略...
    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,
      );
    }
  }
}

function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  const eventPriority = getEventPriority(domEventName);    // 获取事件的优先级
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

function dispatchDiscreteEvent(){}
function dispatchContinuousEvent(){}
function dispatchEvent(){}

// 冒泡
function addEventBubbleListenerWithPassiveFlag(...) {
    target.addEventListener(eventType, listener, {
    passive,
  });
}
function addEventBubbleListener(...) {
    target.addEventListener(eventType, listener, false);
}

// 捕获
function addEventCaptureListenerWithPassiveFlag(...) {
  target.addEventListener(eventType, listener, {
    capture: true,
    passive,
  });
  return listener;
}
function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
){
  target.addEventListener(eventType, listener, true);
  return listener;
}

可以看到实际是addTrappedEventListener做了计算,先是调用 createEventListenerWrapperWithPriority创建了事件listener,然后将listener作为addEventListener的第二个参数,这里要始终记住target永远是容器。

事件绑定

事件绑定是发生在reconiler中的commit阶段,当实例创建成功被推到了父节点和保存在fiber上的stateNode上后,就开始执行处理props信息。 

function completeWork(current, workInProgress, renderLanes) {
    //...省略
    switch (workInProgress.tag) {
        case HostComponent: {
            if(finalizeInitialChildren(...)){
                // ...
            }
        }
    }
}

function finalizeInitialChildren(...){
    setInitialProperties(domElement, type, props);
}

function setInitialProperties(...){
    swith(tag){
        // ...
        case 'source':
        listenToNonDelegatedEvent('error', domElement);
        // ...
    }  
    // ...
}

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js模块中的finalizeInitialChildren只是做了一层代理,最终是在setInitialProperties方法,区分不同的元素类型执行了listenToNonDelegatedEvent方法,在元素本身注册事件,在这里会将在元素上定义的事件如果是可以冒泡的,那么监听的原生事件就是一个空函数,另外还有一些是不能将事件委托到容器上的,也要给元素上监听事件,不过,我们定义的事件函数,最终会被保留在定义事件的元素上,这在后面的事件触发上起到作用。

事件触发

当事件触发,根据事件模型,事件最终会冒泡到容器上被接收,也就是前面的事件注册中的listener,然后开始收集