Synthetic Event
키워드를 발견했을때 낯익음과 낯설음이 공존했다. React에서 버튼을 클릭하는 이벤트를 만들었을때 넘어오는 객체를 콘솔로 찍어보면 나왔던 객체였다. 슬며시 보았을때는 왜 이런 객체가 나오는지 이해가 가지 않았는데 오랜만에 만난김에 정리하기로 하였다.
DispatchQueue
에 추가한다.DispatchQueue
를 반복문으로 순회하면서 캡쳐링 또는 버블링 여부에 따라 순서대로 이벤트를 실행시킨다.HTML 요소의 속성에 직접 inline event를 추가하는 방법이 있다. 하지만 코드가 복잡해지면 유지보수와 관리가 어려워지기 때문에 권장하지 않는다.
<button onclick="alert('Hello, this is my old-fashioned event handler!');">Press me</button>
JavaScript를 사용하여 DOM 요소에 직접 이벤트 핸들러를 추가하여 감지하는 방법이며 주로 사용하는 방법이다.
const bgChange = () => { console.log("클릭") };
const btn = document.querySelector('button');
btn.addEventListener('click', bgChange); // 버튼을 클릭하면 "클릭"이 출력된다.
리액트에서는 합성 이벤트(SyntheticEvent)
라는 객체를 이용하여 이벤트를 처리한다.
합성 이벤트(SyntheticEvent)
DOM의 Event() 생성자로 생성한 이벤트는 브라우저가 생성하는 이벤트와 구분하기 위해 합성 이벤트(synthetic event)라고 부릅니다.
하지만 React에서는 DOM Event(Native Event)를 포함한 래퍼객체SyntheticEvent
를 의미합니다.
브라우저별 호환성(크로스 브라우징)과 사용자의 편의성을 위해서 합성 이벤트를 사용한다.
기존 Event
를 감싼 SyntheticEvent
로 사용되며 일관적으로 이벤트를 처리할 수 있도록 만들었다.
간단하게 jsx 요소 중 DOM 요소에 이벤트핸들러를 추가하고 넘어오는 객체를 확인하면 된다.
<input onChange={(e) => { console.log(e) }} />
결론부터 말하자면 React는 root DOM node에 모든 이벤트 핸들러들을 부착하여 이벤트 위임을 통해 모든 이벤트를 제어한다.
기존 DOM Event는 "mousedown"등 단어와 단어가 소문자로만 이루어져 있기 때문에 카멜케이스로 변경한 후 "on"을 붙여 React에서 제공하는 이벤트 핸들러 네이밍으로 변경한다.
그 후 이벤트마다 0부터 2까지 우선순위를 부여한다. 낮을 수록 높은 우선순위를 가진다.
Priority 0 : DiscreteEvent
Priority 1 : UserBlockingEvent
Priority 2 : ContinuousEvent
<div id="root">
이다. )이 단계들은 앱이 최초로 렌더링 되기 전에 모두 이루어진다.
모든 이벤트를 감시하는 Root Container
button 요소에 이벤트 핸들러를 추가하고 클릭되었을때를 가정해보자
<button onClick={(e) => { console.log(e) }} > 버튼 </button>
button을 클릭하면 리액트에서 ‘click’ 이벤트를 감지하고, 해당 이벤트 리스너가 트리거 된다. 이때, 이 이벤트 리스너는 리액트에서 정의한 dispatchEvent 함수를 호출하게 된다.
호출 시 넘어온 이벤트 객체로 부터 event.target (여기서는 button Node)를 식별하여, 내부적으로 사용하는 키인 internalInstanceKey를 이용하여 해당 DOM Node와 매칭되는 Fiber node를 확인한다.
Fiber node
가 확인되면, 해당 Fiber node
로부터 출발해서 root node까지 Fiber Tree를 순회하며 마치 이벤트 버블링 처럼 매칭되는 이벤트를 가지고있는 Fiber Node를 발견할때마다 이벤트 리스너가 실행 할 함수들을 DispatchQueue
배열로 저장한다.
type DispatchListener = {|
instance: null | Fiber,
listener: Function,
currentTarget: EventTarget,
|};
type DispatchEntry = {|
event: ReactSyntheticEvent,
listeners: Array<DispatchListener>,
|};
export type DispatchQueue = Array<DispatchEntry>;
DispatchQueue
를 반복문으로 순회하면서 event
와 listeners
, inCapturePhase
(이벤트 캡쳐링 여부) 를 추출해 processDispatchQueueItemsInOrder
함수를 실행시킨다. 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();
}
processDispatchQueueItemsInOrder
함수에서는 fiberNode instance, currentTarget, listener함수를 추출하여 이벤트 캡쳐링 여부에 따라 실행 순서를 역전시키고 propagation 여부를 검사 및 이벤트 중복 여부를 확인한 이후에 executeDispatch
함수를 실행시킨다. 즉 이벤트를 실행한다.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;
}
}
}
내용이 너무 어려워서 정리하다가 실수하거나 틀린부분이 있을 수 있는데, 댓글로 알려주시면 감사하겠습니다.
Reference
https://blog.mathpresso.com/react-deep-dive-react-event-system-1-759523d90341
https://gist.github.com/romain-trotard/76313af8170809970daa7ff9d87b0dd5