본문 바로가기
Tech/React

SyntheticEvent and throttle

by egas 2021. 8. 7.

탁구 게임에서 paddle(탁구채)의 위치를 canvas의 onMouseMove 이벤트를 통해 서버에 반영하려고 했다. 그러던 와중에 onMouseMove의 event.currentTarget에서 신기한 현상을 발견했다.

 

아래 동영상은 다음 코드를 출력한 동영상이다.

 

console.log(3, event.currentTarget);

 

보이는가...  null이었다가 어느 순간 currentTarget이 생겨난다. (아래 사진은 event만 출력했을 때 사진이다.)

 

아래는 사용하고있는 react 버전이다.

    "react": "^17.0.2",
    "react-dom": "^17.0.2",

 

우선, currentTarget이 존재하다가 null이 된 것이 아니고, null이었다가 currentTarget이 존재하게 되었다. 또한, currentTarget 속성 외에도 null이어여야 하는데 다른 속성에 값이 들어가 있다. 따라서, React의 Event Pooling 에 대한 이슈는 아니다. 무엇보다 v17 이후 버전이라서, 다른 문제이다.(링크) 그렇다면, 왜 이런 일이 발생하는가?

 

먼저, 다음은 DOM Level 3 Events에 대한 내용이다. (currentTarget은 Event 인터페이스에 속한다.)

https://egas.tistory.com/99

 

DOM Level 3 Events

DOM 스펙은 W3C에서 Level 단위로 만들어지고 있다. DOM 레벨 1은 HTML, XML 문서 구조를 정의하는데 초점이 맞춰져 있었다. 이후 발표된 DOM 레벨 2, 3은 위 구조에 따른 상호작용 기능 추가 및 고급 XML 기

egas.tistory.com

 

또한, DOM Event는 Web APIs 안에 속한다.

https://vimeo.com/96425312

 

가설 1

이벤트가 발생하면 task queue에 추가가 되는데, event loop에 들어가기 전에는 null 상태이고 들어가야 비로소 currentTarget 값이 들어간다.

 

하지만, task queue에 담겼다는 의미는 이벤트가 발생했다는 것이고, 이때 currentTarget을 알지 못한다는 건 자연스러운 흐름이 아니다. 초기화되지 않은 값이 null이지만, 초기화 지연이 일어난다는 이야기인데.. 문서를 확인해보자.

 

우선 MDN 문서 아래에 다음과 같은 부분이 존재한다.

Note: The value of event.currentTarget is only available while the event is being handled. If you console.log() the event object, storing it in a variable, and then look for the currentTarget key in the console, its value will be null. Instead, you can either directly console.log(event.currentTarget) to be able to view it in the console or use the debugger statement, which will pause the execution of your code thus showing you the value of event.currentTarget. - MDN currentTarget -

event.currentTarget 값은 이벤트가 처리되는 동안에만 사용할 수 있습니다. console.log() 이벤트 개체를 변수에 저장한 다음 콘솔에서 currentTarget 키를 찾으면 해당 값은 null이 됩니다. 대신 console.log(event.currentTarget)를 직접 콘솔에서 확인하거나 디버거 문을 사용할 수 있습니다. 그러면 코드 실행이 일시 중지되어 event.currentTarget의 값이 표시됩니다.

 

즉, 이벤트가 처리되지 않는 동안에는 null이 된다는 이야기가 적혀있다.

 

The currentTarget attribute must return the value it was initialized to. When an event is created the attribute must be initialized to null - currentTarget spec -

 

또한, currentTarget spec 에는 currentTarget은 반드시 초기화가 되어야하며, 초기화되지 않으면 null 이어야 한다고 적혀있다.

 

CurrentTarget 속성은 캡처링 및 버블링 단계에서 이벤트 핸들러가 처리되는 요소를 반환한다.

 

즉, null이 나왔다는것은 아직 캡처링, 버블링 단계에 도달하지 못했다는 뜻이다.

 

가설 2

React에서 아직 DOM에 반영을 하지 않았다.

 

React는 브라우저 간 호환성 지원을 제공하기 위해 자체 이벤트 시스템을 구현한다. ReactJS 내에서 이벤트 핸들러를 호출할 때마다 이벤트 핸들러 대신 SyntheticEvent 인스턴스가 전달된다.

 

React 이벤트는 합성 이벤트라서, false를 반환하더라도 이벤트 전파가 멈추지 않는다. 반드시, e.stopPropagation() 또는 e.preventDefault()를 호출해야 한다.

 

또한, React에서 SyntheticEvent는 document Node에 위임되어왔다. 하지만 React 17에서 React는 더 이상 document 레벨에서 이벤트 핸들러를 연결하지 않는다. 대신 React 트리가 렌더링되는 루트 DOM 컨테이너에 연결합니다. (링크)

 

따라서, native event가 먼저 트리거 되고 React의 Root까지 버블링 된 다음에 synthetic events 가 트리거된다.

 

아래 동영상을 보면 root에 currentTarget이 잡힌 것을 볼 수 있다.

console.log(3, event.nativeEvent.currentTarget);

추가적으로 아래 SyntheticEvent에 대한 실험이 있다.

 

https://github.com/hochan222/react-syntheticEvent#react-syntheticevent-bubbling

 

GitHub - hochan222/react-syntheticEvent

Contribute to hochan222/react-syntheticEvent development by creating an account on GitHub.

github.com

여기서 알 수 있는 점은 다음 두 가지이다.

  • React synthetic event handler는 document을 제외한 모든 native dom event handler 발생 이후에 처리된다.
  • document native dom event handler는 항상 모든 React synthetic event handler가 처리된 후 처리된다.

코드를 보자.

    interface BaseSyntheticEvent<E = object, C = any, T = any> {
        nativeEvent: E;
        currentTarget: C;
        target: T;
        bubbles: boolean;
        cancelable: boolean;
        defaultPrevented: boolean;
        eventPhase: number;
        isTrusted: boolean;
        preventDefault(): void;
        isDefaultPrevented(): boolean;
        stopPropagation(): void;
        isPropagationStopped(): boolean;
        persist(): void;
        timeStamp: number;
        type: string;
    }
    /**
     * currentTarget - a reference to the element on which the event listener is registered.
     *
     * target - a reference to the element from which the event was originally dispatched.
     * This might be a child element to the element on which the event listener is registered.
     * If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/11508#issuecomment-256045682
     */
    interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {}

 

세부 interface에 대해서는 본 게시물과 관련된 mouseEvent interface만 보겠다.

 

    interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> {
        detail: number;
        view: AbstractView;
    }
    interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> {
        altKey: boolean;
        button: number;
        buttons: number;
        clientX: number;
        clientY: number;
        ctrlKey: boolean;
        /**
         * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
         */
        getModifierState(key: string): boolean;
        metaKey: boolean;
        movementX: number;
        movementY: number;
        pageX: number;
        pageY: number;
        relatedTarget: EventTarget | null;
        screenX: number;
        screenY: number;
        shiftKey: boolean;
    }

 

해당 코드를 탐방하던 중 나와 같은 현상을 제보한 Issue에서 다음과 같은 사항을 알 수 있었다. (2017년 기준 이슈다.)

  • 현재 event.currentTarget 이벤트를 전달한 후 해제되기 때문에 지속되지 않는다.
  • 이벤트가 지속되는 경우를 제외하고 event.currentTarget이 소멸자에서 해제되기 때문에 이벤트를 전달한 후 null을 할당하는 것을 피할 수 있습니다.

https://github.com/facebook/react/issues/8690

 

event.currentTarget is null in onMouseMove event handler · Issue #8690 · facebook/react

Browser: Chrome LTS React ver: LTS class Dropdown extends PureComponent { constructor(props) { super(props) this.handleMouseMove = this.handleMouseMove.bind(this) this.handleThrottleMouseMove = thr...

github.com

현재 코드는 어떻게 작성 되어있는지 확인해보자.

 

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/DOMPluginEventSystem.js#L221
function executeDispatch(
  event: ReactSyntheticEvent,
  listener: Function,
  currentTarget: EventTarget,
): void {
  const type = event.type || 'unknown-event';
  event.currentTarget = currentTarget;
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
  event.currentTarget = null;
}

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean,
): void {
  let previousInstance;
  if (inCapturePhase) {
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const {instance, currentTarget, listener} = dispatchListeners[i];
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return;
      }
      executeDispatch(event, listener, currentTarget);
      previousInstance = instance;
    }
  }
}

 

dispatchListeners는 발생 좌표에 등록되어있는 이벤트의 집합이다. dispatchListeners를 순회하면서 executeDispatch로 하나하나 실행을 한다. 이때, executeDispatch 안에서 currentTarget을 null로 초기화해준다.

 

여기서 알 수 있는점은 currentTarget은 실행 시점 이후가 되면 null로 초기화된다는 점이다.

 

더 타고 올라가 보겠다.

 

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/DOMPluginEventSystem.js#L259
export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags,
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  for (let i = 0; i < dispatchQueue.length; i++) {
    const {event, listeners} = dispatchQueue[i];
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
    //  event system doesn't use pooling.
  }
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError();
}
// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/DOMPluginEventSystem.js#L221
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);
}

processDispatchQueue에서 쓰이는 dispatchQueue가 extractEvents에서 할당됨을 알 수 있다.

 

extractEvents 이벤트는 BeforeInputEventPlugin, ChangeEventPlugin, EnterLeaveEventPlugin, SelectEventPlugin, SimpleEventPlugin에 정의되어있는 extractEvents들을 실행함을 볼 수 있다. dom event의 종류에 따라 아래 5개의 파일들로 나뉜다. 해당 게시물에서는 mousemove에만 관심 있으므로 mousemove 가 있는 SimpleEventPlugin을 보도록 하자.

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/DOMPluginEventSystem.js#L221
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
) {
  // TODO: we should remove the concept of a "SimpleEventPlugin".
  // This is the basic functionality of the event system. All
  // the other plugins are essentially polyfills. So the plugin
  // should probably be inlined somewhere and have its logic
  // be core the to event system. This would potentially allow
  // us to ship builds of React without the polyfilled plugins below.
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer,
  );
  const shouldProcessPolyfillPlugins =
    (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0;
  // We don't process these events unless we are in the
  // event's native "bubble" phase, which means that we're
  // not in the capture phase. That's because we emulate
  // the capture phase here still. This is a trade-off,
  // because in an ideal world we would not emulate and use
  // the phases properly, like we do with the SimpleEvent
  // plugin. However, the plugins below either expect
  // emulation (EnterLeave) or use state localized to that
  // plugin (BeforeInput, Change, Select). The state in
  // these modules complicates things, as you'll essentially
  // get the case where the capture phase event might change
  // state, only for the following bubble event to come in
  // later and not trigger anything as the state now
  // invalidates the heuristics of the event plugin. We
  // could alter all these plugins to work in such ways, but
  // that might cause other unknown side-effects that we
  // can't foresee right now.
  if (shouldProcessPolyfillPlugins) {
    EnterLeaveEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer,
    );
    ChangeEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer,
    );
    SelectEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer,
    );
    BeforeInputEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer,
    );
  }
}

 

아래 코드를 보면 mousemove일 때, SyntheticEventCtor에 SyntheticMouseEvent를 할당하는 것을 볼 수 있다.

 

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/plugins/SimpleEventPlugin.js
function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName);
  if (reactName === undefined) {
    return;
  }
  let SyntheticEventCtor = SyntheticEvent;
  let reactEventType: string = domEventName;
  switch (domEventName) {
    case 'keypress':
      // Firefox creates a keypress event for function keys too. This removes
      // the unwanted keypress events. Enter is however both printable and
      // non-printable. One would expect Tab to be as well (but it isn't).
      if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
        return;
      }
    /* falls through */
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent;
      break;
    case 'focusin':
      reactEventType = 'focus';
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case 'focusout':
      reactEventType = 'blur';
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case 'beforeblur':
    case 'afterblur':
      SyntheticEventCtor = SyntheticFocusEvent;
      break;
    case 'click':
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return;
      }
    /* falls through */
    case 'auxclick':
    case 'dblclick':
    case 'mousedown':
    case 'mousemove':
    case 'mouseup':
    // TODO: Disabled elements should not respond to mouse events
    /* falls through */
    case 'mouseout':
    case 'mouseover':
    case 'contextmenu':
      SyntheticEventCtor = SyntheticMouseEvent;
      break;
    case 'drag':
    case 'dragend':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'dragstart':
    case 'drop':
      SyntheticEventCtor = SyntheticDragEvent;
      break;
    case 'touchcancel':
    case 'touchend':
    case 'touchmove':
    case 'touchstart':
      SyntheticEventCtor = SyntheticTouchEvent;
      break;
    case ANIMATION_END:
    case ANIMATION_ITERATION:
    case ANIMATION_START:
      SyntheticEventCtor = SyntheticAnimationEvent;
      break;
    case TRANSITION_END:
      SyntheticEventCtor = SyntheticTransitionEvent;
      break;
    case 'scroll':
      SyntheticEventCtor = SyntheticUIEvent;
      break;
    case 'wheel':
      SyntheticEventCtor = SyntheticWheelEvent;
      break;
    case 'copy':
    case 'cut':
    case 'paste':
      SyntheticEventCtor = SyntheticClipboardEvent;
      break;
    case 'gotpointercapture':
    case 'lostpointercapture':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'pointerup':
      SyntheticEventCtor = SyntheticPointerEvent;
      break;
    default:
      // Unknown event. This is used by createEventHandle.
      break;
  }

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {
    const listeners = accumulateEventHandleNonManagedNodeListeners(
      // TODO: this cast may not make sense for events like
      // "focus" where React listens to e.g. "focusin".
      ((reactEventType: any): DOMEventName),
      targetContainer,
      inCapturePhase,
    );
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      dispatchQueue.push({event, listeners});
    }
  } else {
    // Some events don't bubble in the browser.
    // In the past, React has always bubbled them, but this can be surprising.
    // We're going to try aligning closer to the browser behavior by not bubbling
    // them in React either. We'll start by not bubbling onScroll, and then expand.
    const accumulateTargetOnly =
      !inCapturePhase &&
      // TODO: ideally, we'd eventually add all events from
      // nonDelegatedEvents list in DOMPluginEventSystem.
      // Then we can remove this special list.
      // This is a breaking change that can wait until React 18.
      domEventName === 'scroll';

    const listeners = accumulateSinglePhaseListeners(
      targetInst,
      reactName,
      nativeEvent.type,
      inCapturePhase,
      accumulateTargetOnly,
      nativeEvent,
    );
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget,
      );
      dispatchQueue.push({event, listeners});
    }
  }
}

 

따라가 보자.

 

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/SyntheticEvent.js#L178
/**
 * @interface MouseEvent
 * @see http://www.w3.org/TR/DOM-Level-3-Events/
 */
const MouseEventInterface: EventInterfaceType = {
  ...UIEventInterface,
  screenX: 0,
  screenY: 0,
  clientX: 0,
  clientY: 0,
  pageX: 0,
  pageY: 0,
  ctrlKey: 0,
  shiftKey: 0,
  altKey: 0,
  metaKey: 0,
  getModifierState: getEventModifierState,
  button: 0,
  buttons: 0,
  relatedTarget: function(event) {
    if (event.relatedTarget === undefined)
      return event.fromElement === event.srcElement
        ? event.toElement
        : event.fromElement;

    return event.relatedTarget;
  },
  movementX: function(event) {
    if ('movementX' in event) {
      return event.movementX;
    }
    updateMouseMovementPolyfillState(event);
    return lastMovementX;
  },
  movementY: function(event) {
    if ('movementY' in event) {
      return event.movementY;
    }
    // Don't need to call updateMouseMovementPolyfillState() here
    // because it's guaranteed to have already run when movementX
    // was copied.
    return lastMovementY;
  },
};
export const SyntheticMouseEvent = createSyntheticEvent(MouseEventInterface);

 

DOM Level 3 라면 EventInterface에 currentTarget이 정의되어 있을 것이다. 따라가 보자.

 

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/SyntheticEvent.js#L138
/**
 * @interface Event
 * @see http://www.w3.org/TR/DOM-Level-3-Events/
 */
const EventInterface = {
  eventPhase: 0,
  bubbles: 0,
  cancelable: 0,
  timeStamp: function(event) {
    return event.timeStamp || Date.now();
  },
  defaultPrevented: 0,
  isTrusted: 0,
};
export const SyntheticEvent = createSyntheticEvent(EventInterface);

const UIEventInterface: EventInterfaceType = {
  ...EventInterface,
  view: 0,
  detail: 0,
};
export const SyntheticUIEvent = createSyntheticEvent(UIEventInterface);

 

후.. 왜 없지.. 이상하다. 다시 createSyntheticEvent 함수를 추적해보자.

 

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/SyntheticEvent.js#L26
// This is intentionally a factory so that we have different returned constructors.
// If we had a single constructor, it would be megamorphic and engines would deopt.
function createSyntheticEvent(Interface: EventInterfaceType) {
  /**
   * Synthetic events are dispatched by event plugins, typically in response to a
   * top-level event delegation handler.
   *
   * These systems should generally use pooling to reduce the frequency of garbage
   * collection. The system should check `isPersistent` to determine whether the
   * event should be released into the pool after being dispatched. Users that
   * need a persisted event should invoke `persist`.
   *
   * Synthetic events (and subclasses) implement the DOM Level 3 Events API by
   * normalizing browser quirks. Subclasses do not necessarily have to implement a
   * DOM interface; custom application-specific events can also subclass this.
   */
  function SyntheticBaseEvent(
    reactName: string | null,
    reactEventType: string,
    targetInst: Fiber,
    nativeEvent: {[propName: string]: mixed},
    nativeEventTarget: null | EventTarget,
  ) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;

    for (const propName in Interface) {
      if (!Interface.hasOwnProperty(propName)) {
        continue;
      }
      const normalize = Interface[propName];
      if (normalize) {
        this[propName] = normalize(nativeEvent);
      } else {
        this[propName] = nativeEvent[propName];
      }
    }

    const defaultPrevented =
      nativeEvent.defaultPrevented != null
        ? nativeEvent.defaultPrevented
        : nativeEvent.returnValue === false;
    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }
    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  Object.assign(SyntheticBaseEvent.prototype, {
    preventDefault: function() {
      this.defaultPrevented = true;
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.preventDefault) {
        event.preventDefault();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.returnValue !== 'unknown') {
        event.returnValue = false;
      }
      this.isDefaultPrevented = functionThatReturnsTrue;
    },

    stopPropagation: function() {
      const event = this.nativeEvent;
      if (!event) {
        return;
      }

      if (event.stopPropagation) {
        event.stopPropagation();
        // $FlowFixMe - flow is not aware of `unknown` in IE
      } else if (typeof event.cancelBubble !== 'unknown') {
        // The ChangeEventPlugin registers a "propertychange" event for
        // IE. This event does not support bubbling or cancelling, and
        // any references to cancelBubble throw "Member not found".  A
        // typeof check of "unknown" circumvents this issue (and is also
        // IE specific).
        event.cancelBubble = true;
      }

      this.isPropagationStopped = functionThatReturnsTrue;
    },

    /**
     * We release all dispatched `SyntheticEvent`s after each event loop, adding
     * them back into the pool. This allows a way to hold onto a reference that
     * won't be added back into the pool.
     */
    persist: function() {
      // Modern event system doesn't use pooling.
    },

    /**
     * Checks if this event should be released back into the pool.
     *
     * @return {boolean} True if this should not be released, false otherwise.
     */
    isPersistent: functionThatReturnsTrue,
  });
  return SyntheticBaseEvent;
}

 

찾았다. createSyntheticEvent는 여러 종류의 인터페이스에 맞는 constructor를 반환하기 위한 팩토리 메서드이다. createSyntheticEvent SyntheticBaseEvent 함수를 반환하는 함수이다.

 

SyntheticBaseEvent 함수 안에서 this.currentTarget = null; 처리를 해주고 있다. 

 

SyntheticEvent는 일반적으로 최상위 이벤트 위임 핸들러에 대한 응답으로 이벤트 플러그인에 의해 전달된다.이러한 시스템은 일반적으로 풀링을 사용하여 garbage collection 빈도를 줄여야 한다. v17 이전까지는 이벤트 풀링을 했지만, v17부터는 코드에서 보다시피 흔적만 남겨놓았다. v17 이후의 동작은, 각 이벤트 루프 후에 전달된 모든 `SyntheticEvent`를 해제하고 다시 풀에 추가한다. 이렇게 하면 풀에 다시 추가되지 않는 참조를 유지할 수 있다. 

 

React에는 동일한 event loop tick에서 여러 업데이트를 묶어주는 데 사용하는 unstable_batchedUpdates API가 있다. 즉, React 이벤트 루프는 React가 가장 효율적으로 결정된 시간에 변경 사항을 일괄 처리하고 적용하는데 이 기능을 수행하는 자체 이벤트 루프를 뜻한다.

 

SyntheticEvent(및 하위 클래스)는 브라우저 쿼크를 정규화하여 DOM 레벨 3 이벤트 API를 구현한다. 그렇다고 서브클래스가 반드시 DOM 인터페이스를 구현할 필요는 없다.


한편, currentTarget이 할당되는 코드를 보자.

// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/DOMPluginEventSystem.js#L644
function createDispatchListener(
  instance: null | Fiber,
  listener: Function,
  currentTarget: EventTarget,
): DispatchListener {
  return {
    instance,
    listener,
    currentTarget,
  };
}
// https://github.com/facebook/react/blob/c96761c7b217989a6c377c9b12249a78b0be91f9/packages/react-dom/src/events/DOMPluginEventSystem.js#L767
// We should only use this function for:
// - BeforeInputEventPlugin
// - ChangeEventPlugin
// - SelectEventPlugin
// This is because we only process these plugins
// in the bubble phase, so we need to accumulate two
// phase event listeners (via emulation).
export function accumulateTwoPhaseListeners(
  targetFiber: Fiber | null,
  reactName: string,
): Array<DispatchListener> {
  const captureName = reactName + 'Capture';
  const listeners: Array<DispatchListener> = [];
  let instance = targetFiber;

  // Accumulate all instances and listeners via the target -> root path.
  while (instance !== null) {
    const {stateNode, tag} = instance;
    // Handle listeners that are on HostComponents (i.e. <div>)
    if (tag === HostComponent && stateNode !== null) {
      const currentTarget = stateNode;
      const captureListener = getListener(instance, captureName);
      if (captureListener != null) {
        listeners.unshift(
          createDispatchListener(instance, captureListener, currentTarget),
        );
      }
      const bubbleListener = getListener(instance, reactName);
      if (bubbleListener != null) {
        listeners.push(
          createDispatchListener(instance, bubbleListener, currentTarget),
        );
      }
    }
    instance = instance.return;
  }
  return listeners;
}

 

이벤트 함수 호출 순서

마지막으로 아래는 이벤트 함수 호출 순서이다.

dispatchEvent // ReactDOMEventListener.js

dispatchEventForPluginEventSystem // DOMPluginEventSystem.js

batchedEventUpdates // ReactDOMUpdateBatching.js

dispatchEventsForPlugins // DOMPluginEventSystem.js //이 함수는 이벤트를 먼저 합성한 다음 실행한다.

extractEvents // DOMPluginEventSystem.js

// SimpleEventPlugin을 예로 들어

accumulateSinglePhaseListeners // DOMPluginEventSystem.js

processDispatchQueue // dispatchEventsForPlugins 함수에서 호출됨

executeDispatch  // DOMPluginEventSystem.js

invokeGuardedCallbackAndCatchFirstError // shared/ReactErrorUtils

 

TL;DR

결론적으로 해당 동영상의 현상은 extractEvent 부분에서 SyntheticEvent가 생성된다. 이때, currentTarget이 null로 초기화된다. accumulateSinglePhaseListeners 함수에서  currentTarget의 값이 할당된다. React 이벤트 루프는 React가 가장 효율적으로 결정된 시간에 변경 사항을 일괄 처리하고 적용하기 때문에 위와같은 현상이 일어난 것이다.

 

참고

console.log 는 비동기적으로 실행된다. 

https://egas.tistory.com/80

 

This value was evaluated upon first expanding. it may have changed since then.

아래 파란색 `i`를 누르면 This value was evaluated upon first expanding. it may have changed since then. 안내 문구가 나온다. 해당 상황의 경우, 클릭했을 때, console.log로 해당 className을 출력한 뒤,..

egas.tistory.com

 

 

 

 

 

 

 

 

728x90

'Tech > React' 카테고리의 다른 글

useEffect의 Dependency Array 비교 원리  (0) 2022.05.25
react-hot-loader란?  (0) 2021.10.26
ReactElement.js  (2) 2021.08.04
Controlled Components, Uncontrolled components  (0) 2021.08.03
React Element vs Component  (0) 2021.08.01

댓글