нажмитеВойдите в репозиторий отладки исходного кода React.
Зачем вам нужно реализовывать механизм событий самостоятельно?
Из-за особенностей механизма волокна, когда создается узел волокна, соответствующий ему узел DOM может быть не смонтирован, а обработчики событий, такие как onClick, в качестве опоры узла волокна, не могут быть напрямую привязаны к реальному узлу DOM. С этой целью React предоставляет механизм событий «регистрации верхнего уровня, сбора событий и унифицированного запуска».
Так называемая «регистрация верхнего уровня» фактически привязывает унифицированный обработчик событий к корневому элементу. «Сбор событий» относится к моменту запуска события (на самом деле выполняется обработчик события в корне), созданию искусственного объекта события и сбору реального обработчика событий в компоненте в соответствии с путем всплытия или захвата. «Единый запуск» происходит после процесса сбора и выполняет собранные события одно за другим, используя один и тот же синтетический объект события. Важным моментом здесь является то, что обработчик событий, привязанный к корню, — это не тот обработчик событий, который мы написали в компоненте, обратите внимание на это отличие, о котором будет сказано ниже.
Выше приведено краткое описание механизма событий React.Этот механизм позволяет избежать проблемы, заключающейся в том, что события не могут быть напрямую привязаны к узлам DOM, и может эффективно использовать иерархические отношения дерева волокон для создания путей выполнения событий, тем самым имитируя захват событий. и риск, а также два очень важных свойства:
- Классифицировать события, которые могут включать разные приоритеты задач, вызвавших событие.
- Предоставляет синтетические объекты событий для сглаживания различий совместимости браузеров.
В этой статье подробно объясняется механизм событий на протяжении всего жизненного цикла события от регистрации до выполнения.
регистрация на мероприятие
В отличие от предыдущих версий, события React17 регистрируются в корневом каталоге, а не в документе, в основном для постепенных обновлений и во избежание конфликтов системы событий в сценариях, где сосуществуют несколько версий React.
Когда мы связываем событие как элемент, вы должны написать:
<div onClick={() => {/*do something*/}}>React</div>
Этот узел div в конечном итоге соответствует узлу волокна, а onClick используется в качестве его реквизита. Когда узел волокна входит в полную фазу фазы рендеринга, реквизит с именем onClick будет распознан как событие для обработки.
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
...
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {
// 如果propKey属于事件类型,则进行事件绑定
ensureListeningTo(rootContainerElement, propKey, domElement);
}
}
}
}
RegistrationNameDependencies — это объект, в котором хранится коллекция собственных событий DOM, соответствующих всем событиям React, что является основой для определения того, является ли реквизит событием. Если это реквизит типа события, то для привязки события будет вызвана функция sureListeningTo.
Следующий процесс связывания можно резюмировать в виде следующих ключевых моментов:
- Найдите зависимость события в соответствии с именем события React.Например, событие onMouseEnter зависит от двух собственных событий, mouseout и mouseover, а onClick зависит только от щелчка одного собственного события, которое в конечном итоге будет перебирать эти зависимости и связывать соответствующие событие в корне. Например, компонент onClick, тогда прослушиватель событий щелчка будет привязан к корню.
- Определите, к какой фазе события (всплытие или захвату) оно принадлежит, на основе имени события, записанного в компоненте, например
onClickCapture
Такое имя события React означает, что событие должно быть запущено на этапе захвата, иonClick
События делегата должны запускаться на этапе всплытия. - По имени события React узнайте соответствующее собственное имя события, например
click
, и в соответствии с предыдущим шагом, чтобы определить, нужно ли его запускать на этапе захвата, вызовитеaddEventListener
, который привязывает событие к корневому элементу. - Если событие необходимо обновить, сначала удалите прослушиватель событий, а затем выполните повторную привязку.Процесс привязки повторяет три вышеуказанных шага.
После этой серии процессов прослушиватель событий окончательно привязывается к корневому элементу.
// 根据事件名称,创建不同优先级的事件监听器。
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
listenerPriority,
);
// 绑定事件
if (isCapturePhaseListener) {
...
unsubscribeListener = addEventCaptureListener(
targetContainer,
domEventName,
listener,
);
} else {
...
unsubscribeListener = addEventBubbleListener(
targetContainer,
domEventName,
listener,
);
}
Кто прослушиватель событий
При привязке события, упомянутого выше, функция прослушивателя событий, привязанная к корню, является слушателем, но этот слушатель не является обработчиком событий, который мы написали непосредственно в компоненте. Как видно из приведенного выше кода, слушательcreateEventListenerWrapperWithPriority
результат звонка
Зачем создавать такой слушатель вместо прямой привязки обработчика событий, написанного в компоненте?
фактическиcreateEventListenerWrapperWithPriority
В названии функции уже содержится ответ: создайте оболочку прослушивателя событий на основе приоритета. Есть два важных момента:приоритета такжеоболочка прослушивателя событий. Приоритет здесь относится к приоритету события (для подробного ознакомления с приоритетом события перейдите кПриоритет в реакции).
Приоритеты событий делятся в соответствии со степенью взаимодействия событий, а отношение сопоставления между приоритетами и именами событий существует в структуре карты.createEventListenerWrapperWithPriority
Он будет возвращать различные уровни событий в соответствии с именем события или входящим приоритетом.оболочка прослушивателя событий.
Всего будет три обёртки слушателей событий:
- dispatchDiscreteEvent: обрабатывать дискретные события
- dispatchUserBlockingUpdate: обрабатывать события блокировки пользователя
- dispatchEvent: обрабатывать непрерывные события
Эти обертки являются прослушивателями событий, которые фактически привязаны к корню, и у них есть свои собственные приоритеты.Когда запускается соответствующее событие, фактически вызывается прослушиватель событий с приоритетом.
Прозрачный флаг этапа выполнения события передачи
Здесь сначала разберемся, слушатель событий с приоритетом привязан к корню, его срабатывание приведет к срабатыванию реального события в компоненте. Но одна вещь, которая до сих пор не была рассмотрена, — это различие между фазами выполнения события. Хотя зарегистрированное событие в компоненте может отличать будущую стадию выполнения в форме имени события + суффикса «Capture», это на самом деле отличается от фактического выполнения события, поэтому ключ теперь заключается в том, как явно реализовать стадию выполнения. объявляется при регистрации события Поведение выполнения события.
Мы можем обратить на это вниманиеcreateEventListenerWrapperWithPriority
Один из параметров функции: eventSystemFlags. Это знак событийной системы, фиксирующий различные признаки событий, одним из которых являетсяIS_CAPTURE_PHASE, что указывает на то, что текущее событие инициировано фазой захвата. Когда имя события содержит суффикс Capture, для eventSystemFlags будет присвоено значение IS_CAPTURE_PHASE.
Позже, при создании прослушивателя событий, привязанного к root с приоритетом, eventSystemFlags будет передан в качестве входного параметра при его выполнении. Поэтому, когда событие запускается, вы можете знать, что события в компоненте выполняются в порядке всплытия или захвата.
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
...
discreteUpdates(
dispatchEvent,
domEventName,
eventSystemFlags, // 传入事件执行阶段的标志
container,
nativeEvent,
);
}
резюме
Теперь мы должны прояснить две вещи:
- Функция обработки событий привязана не к элементу компонента, а к корню, что связано со структурными характеристиками дерева файбера, то есть функция обработки событий может использоваться только как опора файбера.
- Слушатель событий, привязанный к корню, — это не обработчик событий, который мы написали в компоненте, а слушатель, который удерживает приоритет события и может передавать флаг стадии выполнения события.
Теперь, когда этап регистрации завершен, давайте поговорим о том, как запускается событие, давайте перейдем от слушателя, привязанного к корню, и посмотрим, что он делает.
Инициирование событий — что делает прослушиватель событий
Что он делает, можно резюмировать одним предложением: он отвечает за запуск реального потока событий с разными весами приоритета и передачу флагов этапа выполнения события (eventSystemFlags).
Например, если элемент привязан к событию onClick, при щелчке по нему будет запущен прослушиватель, привязанный к корню, что в конечном итоге приведет к выполнению события в компоненте.
Другими словами, прослушиватель событий, связанный с корнем, просто эквивалентен вестнику, который следует за событием.приоритетЧтобы запланировать следующее задание:Синтез событийных объектов,Собирать обработчики событий в пути выполнения,выполнение события, чтобы в последующем процессе планирования планировщик мог узнать приоритет текущей задачи, а затем расширить планирование.
Как пройти приоритет?
Используйте планировщик вrunWithPriority
функция, вызывая ее, приоритет записывается в планировщик, поэтому планировщик может знать приоритет текущей задачи при планировании.runWithPriority
Второй параметр пойдет на организацию трех упомянутых выше заданий.
В качестве примера возьмем уровень приоритета блокировки пользователей:
function dispatchUserBlockingUpdate(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
...
runWithPriority(
UserBlockingPriority,
dispatchEvent.bind(
null,
domEventName,
eventSystemFlags,
container,
nativeEvent,
),
);
}
dispatchUserBlockingUpdate вызывает runWithPriority и передает приоритет UserBlockingPriority, так что приоритет UserBlockingPriority может быть записан в планировщике, и последующее вычисление React различных приоритетов основано на этом приоритете UserBlockingPriority.
Помимо передачи приоритета, другая важная вещь, которую он делает, это запускаетСинтез событийных объектов,Собирать обработчики событий в пути выполнения,выполнение событияЭти три процесса, то есть на стадии исполнения события. Слушатель событий на корне, наконец, срабатывает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);
}
dispatchEventsForPlugins
Поток событий в функции имеет важный носитель: dispatchQueue, который несет в себе объект события, синтезированный на этот раз, и функцию обработки события, собранную на пути выполнения события.
listeners — это путь выполнения события, event — синтетический объект события, реальные события в компоненте собираются в путь выполнения, а синтез объекта события реализуется с помощью ExtractEvents.
Синтез объектов событий и сбор событий
Здесь нам должно быть ясно, что когда прослушиватель событий в корне срабатывает, он запускает процесс синтеза объекта события и сбора событий, который должен подготовиться к реальному запуску события.
синтетический объект события
Объект события, полученный в функции обработчика событий в компоненте, не является нативным объектом события, а синтезируется ReactSyntheticEvent
объект. Он устраняет различия совместимости между различными браузерами. Абстрагируйте его в единый объект события, чтобы облегчить умственную нагрузку разработчиков.
путь выполнения события
Когда объект события синтезирован, событие будет собрано на пути выполнения события. Каков путь выполнения события?
В среде браузера, если родительский и дочерний элементы привязаны к событиям одного и того же типа, за исключением ручного вмешательства, эти события будут запускаться в порядке всплытия или захвата.
То же самое верно и в React, начиная с элемента, который запускает событие, ища в соответствии с иерархической структурой дерева волокон, накапливая все события одного и того же типа в элементе верхнего уровня и, наконец, формируя массив со всеми события одного типа.Этот массив представляет собой путь выполнения события. По этому пути React сам имитирует набор механизмов захвата и всплытия событий.
На следующем рисунке показан общий процесс упаковки объекта события и сбора событий (например, путь всплытия).
Поскольку разные события имеют разное поведение и механизмы обработки, создание искусственных объектов событий и сбор событий на пути выполнения необходимо реализовать с помощью подключаемых модулей. Всего есть 5 плагинов:SimpleEventPlugin, EnterLeaveEventPlugin, ChangeEventPlugin, SelectEventPlugin, BeforeInputEventPlugin. Их миссии точно такие же, но типы событий, с которыми они справляются, разные, поэтому будут некоторые внутренние различия. В этой статье используется толькоSimpleEventPlugin
В качестве примера, чтобы проиллюстрировать этот процесс, он обрабатывает более общие типы событий, такие какclick、input、keydown
Ждать.
Ниже приведен код синтетического объекта события в SimpleEventPlugin, который собирает событие.
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 EventInterface;
switch (domEventName) {
// 赋值EventInterface(接口)
}
// 构造合成事件对象
const event = new SyntheticEvent(
reactName,
null,
nativeEvent,
nativeEventTarget,
EventInterface,
);
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
if (/*...*/) {
...
} else {
// scroll事件不冒泡
const accumulateTargetOnly =
!inCapturePhase &&
domEventName === 'scroll';
// 事件对象分发 & 收集事件
accumulateSinglePhaseListeners(
targetInst,
dispatchQueue,
event,
inCapturePhase,
accumulateTargetOnly,
);
}
return event;
}
Создайте синтетический объект события
Этот унифицированный объект события представленSyntheticEvent
Он следует спецификации W3C и снова реализует интерфейс объекта события браузера, так что разница может быть сглажена, а собственный объект события является лишь одним из его свойств (nativeEvent).
// 构造合成事件对象
const event = new SyntheticEvent(
reactName,
null,
nativeEvent,
nativeEventTarget,
EventInterface,
);
Собирать события в путь выполнения
Этот процесс заключается в том, чтобы собрать реальные обработчики событий в компоненте в массив и дождаться выполнения следующего пакета.
Давайте рассмотрим пример: целевой элемент — это counter, а родительский элемент — counter-parent.
class EventDemo extends React.Component{
state = { count: 0 }
onDemoClick = () => {
console.log('counter的点击事件被触发了');
this.setState({
count: this.state.count + 1
})
}
onParentClick = () => {
console.log('父级元素的点击事件被触发了');
}
render() {
const { count } = this.state
return <div
className={'counter-parent'}
onClick={this.onParentClick}
>
<div
onClick={this.onDemoClick}
className={'counter'}
>
{count}
</div>
</div>
}
}
При щелчке счетчика также запускается событие щелчка родительского элемента, которое выводит:
'counter的点击事件被触发了'
'父级元素的点击事件被触发了'
На самом деле это результат сбора событий в порядке всплытия после пути выполнения. Процесс сбора состоит изaccumulateSinglePhaseListeners
Заканчивать.
accumulateSinglePhaseListeners(
targetInst,
dispatchQueue,
event,
inCapturePhase,
accumulateTargetOnly,
);
Важнейшей операцией внутри функции, несомненно, является сбор событий на путь выполнения, для реализации этой операции необходимо начать с исходного узла волокна, который запускает событие в дереве волокон, и найти корень всех событий. путь вверх, чтобы сформировать полный пузырьковый путь или путь захвата. При этом при прохождении узла файбера по пути по названию события получить из пропса функцию обработки события, которую мы собственно прописали в компоненте, протолкнуть ее в путь и дождаться выполнения следующего батча.
Ниже приведен упрощенный исходный код процесса.
export function accumulateSinglePhaseListeners(
targetFiber: Fiber | null,
dispatchQueue: DispatchQueue,
event: ReactSyntheticEvent,
inCapturePhase: boolean,
accumulateTargetOnly: boolean,
): void {
// 根据事件名来识别是冒泡阶段的事件还是捕获阶段的事件
const bubbled = event._reactName;
const captured = bubbled !== null ? bubbled + 'Capture' : null;
// 声明存放事件监听的数组
const listeners: Array<DispatchListener> = [];
// 找到目标元素
let instance = targetFiber;
// 从目标元素开始一直到root,累加所有的fiber对象和事件监听。
while (instance !== null) {
const {stateNode, tag} = instance;
if (tag === HostComponent && stateNode !== null) {
const currentTarget = stateNode;
// 事件捕获
if (captured !== null && inCapturePhase) {
// 从fiber中获取事件处理函数
const captureListener = getListener(instance, captured);
if (captureListener != null) {
listeners.push(
createDispatchListener(instance, captureListener, currentTarget),
);
}
}
// 事件冒泡
if (bubbled !== null && !inCapturePhase) {
// 从fiber中获取事件处理函数
const bubbleListener = getListener(instance, bubbled);
if (bubbleListener != null) {
listeners.push(
createDispatchListener(instance, bubbleListener, currentTarget),
);
}
}
}
instance = instance.return;
}
// 收集事件对象
if (listeners.length !== 0) {
dispatchQueue.push(createDispatchEntry(event, listeners));
}
}
Независимо от того, выполняется ли событие на этапе всплытия или на этапе захвата, оно передается слушателям dispatchQueue в одном и том же порядке, а порядок выполнения событий всплытия или захвата отличается, поскольку порядок очистки массива слушателей отличается.
Обратите внимание, что каждая коллекция будет собирать только события того же типа, что и источник события, например привязку подэлемента onClick, родительский элемент привязан к onClick и onClickCapture:
<div
className="parent"
onClick={onClickParent}
onClickCapture={onClickParentCapture}
>
父元素
<div
className="child"
onClick={onClickChild}
>
子元素
</div>
</div>
Итак, когда вы нажмете на дочерний элемент, он будетonClickChild
а такжеonClickParent
.
Собранные результаты следующие
Как объект синтетического события участвует в процессе выполнения события
Как мы сказали выше, структура dispatchQueue выглядит следующим образом
[
{
event: SyntheticEvent,
listeners: [ listener1, listener2, ... ]
}
]
event представляет синтетический объект события, который можно рассматривать как объект события, совместно используемый этими слушателями. Когда массив слушателей очищается и выполняется для каждой функции прослушивателя событий, прослушиватель событий может изменить currentTarget для события или вызвать для него метод stopPropagation, чтобы предотвратить всплытие. Как общий ресурс, события отслеживаются и потребляются этими событиями, а поведение потребления происходит при выполнении события.
выполнение события
После процесса сбора событий и объектов событий получают полный путь выполнения события и общий объект события, а затем вступают в процесс выполнения события, зацикливают путь от начала до конца и вызывают функцию слушателя в каждом элементе по очереди. Основное внимание в этом процессе уделяется моделированию всплытия и захвата событий, а также применению искусственных объектов событий.Следующее представляет собой процесс извлечения объектов событий и путей выполнения времени из dispatchQueue.
export function processDispatchQueue(
dispatchQueue: DispatchQueue,
eventSystemFlags: EventSystemFlags,
): void {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
// 从dispatchQueue中取出事件对象和事件监听数组
const {event, listeners} = dispatchQueue[i];
// 将事件监听交由processDispatchQueueItemsInOrder去触发,同时传入事件对象供事件监听使用
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
}
// 捕获错误
rethrowCaughtError();
}
Имитация пузырьков и захвата
Порядок выполнения всплытия и захвата различается, но при сборе событий, будь то всплытие или захват, события напрямую помещаются в путь. Так как же отражается разница в порядке выполнения? Ответ заключается в том, что порядок путей цикла отличается, что приводит к другому порядку выполнения.
Во-первых, просмотрите порядок обработчиков событий в слушателях в dispatchQueue: обработчики событий целевого элемента, который запускает событие, располагаются первыми, а обработчики событий компонентов верхнего уровня располагаются по порядку.
<div onClick={onClickParent}>
父元素
<div onClick={onClickChild}>
子元素
</div>
</div>
listeners: [ onClickChild, onClickParent ]
При циклировании слева направо сначала срабатывает событие целевого элемента, а события родительского элемента выполняются по очереди.Это то же самое, что и порядок всплытия, а порядок захвата естественно циклически повторяется справа налево. осталось. Код для имитации всплытия и захвата события выполнения выглядит следующим образом:
Основание для суждения о стадии исполнения мероприятияinCapturePhase, источник которого указан вышеПрозрачный флаг этапа выполнения события передачиуже упоминалось в содержании.
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;
}
// 执行事件,传入event对象,和currentTarget
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
} else {
// 事件冒泡正序循环
for (let i = 0; i < dispatchListeners.length; i++) {
const {instance, currentTarget, listener} = dispatchListeners[i];
// 如果事件对象阻止了冒泡,则return掉循环过程
if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}
}
В этот момент функция обработчика событий, которую мы написали в компоненте, была выполнена.Синтетический объект события выступает в качестве общедоступной роли в этом процессе.При выполнении каждого события синтетический объект события будет проверяться, чтобы увидеть, есть ли какой-либо метод для предотвращения , кроме того, элемент, который в данный момент присоединен к прослушивателю событий, будет смонтирован на объекте события как currentTarget и, наконец, передан в обработчик события, мы можем получить объект события.
Суммировать
Объем кода в системе событий в исходниках очень большой.Я могу прожить код в основном с такими вопросами: каков процесс привязки событий, связь между системой событий и приоритетом, реальная функция обработки событий Как именно это работает.
Кратко опишите принцип механизма событий: из-за характеристик дерева волокон, если компонент содержит свойство события, он привязывает прослушиватель событий к корню на этапе фиксации соответствующего узла волокна.Этот прослушиватель событий имеет приоритет. Это связывает его с механизмом приоритетов, который может использовать механизм синтетических событий в качестве координатора, отвечающего за координацию.Синтезировать объекты событий, собирать события, запускать обработчики реальных событийэти три процесса.