CREATED DAY:20230217

实现合成事件

类似实现

源码地址: https://github.com/Hujianboo/react-replica

在 createRoot 开始阶段,我们会初始化事件分发中心,并且将其所有的事件类型,在 Container 上监听一遍,在 completeWork 阶段创建真实 dom 的时候,将事件回调函数直接挂在真实 dom 的某个属性上,比如就叫 __props 它里面存放每个 ReactElement 上的 props。

const elementPropsKey = "__props";
interface DomElement extends Element {
  [elementPropsKey]: Props;
}
export function updateFiberProps(node: DOMElement, props: Props) {
  node[elementPropsKey] = props;
}

updateFiberProps 函数会赋值或者更新 dom 节点的__props 属性值。我们需要在创建和更新 dom 节点的时候去调用它,也就是在 complete 阶段。

case HostComponent:
			if (current !== null && wip.stateNode) {
				// update
				updateFiberProps(wip.stateNode, newProps);
			} else {
				// mount
				const instance = createInstance(wip.type, newProps);
				appendAllChildren(instance, wip);
				wip.stateNode = instance;
			}
			bubbleProperties(wip);
			return null;

事件流程图.png

至此,准备工作完毕。当用户进行交互,比如点击了一个带 onClick 的 ReactElement 按钮时,原生事件会不断地冒泡至 Container,触发了 EventDispatch 中 click 的事件回调

事件原理.png

function dispatchEvent(container: Container, eventType: string, e: Event) {
  const targetElement = e.target;

  if (targetElement === null) {
    return;
  }

  // 1. 收集事件
  const { bubble, capture } = collectPaths(
    targetElement as DOMElement,
    container,
    eventType
  );
  // 2. 构造合成事件
  const se = createSyntheticEvent(e);

  // 3. 遍历captue和bubble
  triggerEventFlow(capture, se);
  //如果
  if (!se.__stopPropagation) {
    triggerEventFlow(bubble, se);
  }
}

该回调里面是一个执行所有合成事件的方法,它主要做四件事情,1.收集从 target Dom 到 Container 的所有沿途的事件 2.构造生成合成事件 3.遍历执行所有的捕获事件和冒泡事件。

function collectPaths(
  targetElement: DOMElement,
  container: Container,
  eventType: string
) {
  const paths: Paths = {
    capture: [],
    bubble: [],
  };

  while (targetElement && targetElement !== container) {
    // 收集
    const elementProps = targetElement[elementPropsKey];
    if (elementProps) {
      // click -> onClick onClickCapture
      const callbackNameList = getEventCallbackNameFromEventType(eventType);
      if (callbackNameList) {
        callbackNameList.forEach((callbackName, i) => {
          const eventCallback = elementProps[callbackName];
          if (eventCallback) {
            if (i === 0) {
              // capture
              paths.capture.unshift(eventCallback);
            } else {
              paths.bubble.push(eventCallback);
            }
          }
        });
      }
    }
    targetElement = targetElement.parentNode as DOMElement;
  }
  return paths;
}

首先 collectPaths 根据传入的 targetElement,不停向上循环遍历直到 container,在循环中会收集每个 dom 的事件回调,同时收集捕获事件和冒泡事件,存放到对应数组中。

function createSyntheticEvent(e: Event) {
  const syntheticEvent = e as SyntheticEvent;
  syntheticEvent.__stopPropagation = false;
  const originStopPropagation = e.stopPropagation.bind(e);

  syntheticEvent.stopPropagation = () => {
    syntheticEvent.__stopPropagation = true;
    if (originStopPropagation) {
      originStopPropagation();
    }
  };
  return syntheticEvent;
}

接下来我们要创建合成事件。因为虽然得到了所有的冒泡和捕获事件,但是却还需要实现一些别的特性,比如阻止冒泡这种行为,在触发收集的事件时,我们就通过合成事件来判断是否有阻止冒泡行为。这里很简单,直接给对象时间添加一个stopPropagation 的布尔属性。并且改写原来的 stopPropagation,当执行 stopPropagation 时,将stopPropagation 的值变为 true,并且执行原来事件对象里的 stopPropagation 函数。

function triggerEventFlow(paths: EventCallback[], se: SyntheticEvent) {
  for (let i = 0; i < paths.length; i++) {
    const callback = paths[i];
    callback.call(null, se);

    if (se.__stopPropagation) {
      break;
    }
  }
}

最后触发所有的收集到的事件。triggerEventFlow 会遍历事件数组,然后取出里面事件执行,并且传参为我们构建的合成事件,其中执行完毕后,如果合成事件的__stopPropagation 的为 true,代表阻止事件传递下去,这里就直接 break 跳出循环。

这里我们用个小 demo 实际验证一下效果

const App = () => {
  const [num, setNum] = useState(10000);
  const [num1, setNum1] = useState(100086);
  return (
    <p>
      <div
        onClick={() => {
          console.log("父亲被冒泡触发");
        }}
      >
        <div
          onClick={(e) => {
            console.log("点击");
            // 阻止事件传递
            e.stopPropagation();
          }}
        >
          点击
        </div>
      </div>
    </p>
  );
};

实际效果如下,所示,可以看到成功触发点击事件,并且阻止了事件传递,父亲组件的 click 回调没有被触发。

demo.jpeg

react18 的事件实现

先讲一下 react18.2 的事件系统是怎么样的,首先和我们类似,都是在 createRoot 初始化事件系统。

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions
): RootType {
  //xxxxx

  listenToAllSupportedEvents(rootContainerElement);
}

listenToAllSupportedEvents 是 18.2 事件系统中用来给 Container 注册事件的,

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);
      }
    }
  }
}

其中的 nonDelegatedEvents 是不支持委托也就是不支持冒泡事件的集合,listenToNativeEvent 的作用是将原生事件绑定到 dom 上,捕获和冒泡的区分逻辑可看上面注释,两个唯一的区别就是在于给 listenToNativeEvent 的第二个参数传参布尔值不同而已。

export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget
): void {
  if (__DEV__) {
    if (nonDelegatedEvents.has(domEventName) && !isCapturePhaseListener) {
      console.error(
        'Did not expect a listenToNativeEvent() call for "%s" in the bubble phase. ' +
          "This is a bug in React. Please file an issue.",
        domEventName
      );
    }
  }

  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
) {
  //省略
  // 对冒泡和捕获进行区分注册
  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
      );
    }
  }
}

可以看到 listenToNativeEvent 里面主要用了 addTrappedEventListener,而 addTrappedEventListener 里则根据 listenToNativeEvent 的第二个参数来进行区分捕获和冒泡,不得不说,这个封装真是跟裹脚布一样长。

当用户交互时,就会触发 Container 上对应事件的 listener,源码这里对应的是 dispatchEventOriginal,也就是跟自己实现的 dispatchEvent 一样,它最终会执行一个叫 dispatchEventsForPlugins 的函数,

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  // 获取事件源
  const nativeEventTarget = getEventTarget(nativeEvent);
  const dispatchQueue: DispatchQueue = [];
  // 收集要执行的事件回调
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  );
  //执行事件
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

它的这个作用就是我们类似实现的沿途收集所有事件回调并执行。这里跟我们的实现最大的不同在于,它在初始化事件系统时,就对冒泡和捕获做好了区分,当用户具体交互的时候,比如一次 click,它就会它会遍历两次回调事件队列(数组),第一次是原生捕获结束后,执行一遍全部的合成捕获回调,第二次是原生冒泡结束后,执行一遍全部的合成冒泡回调。

我们的实现的缺陷在于,如果使用到对原生 dom 的监听,原生的监听的冒泡事件会比我们的合成捕获事件先执行,这个显然是不对的。更好地方式应该是像原生一样对捕获和冒泡分开执行,所以我们的实现还有改进的空间。