«Эта статья участвовала в мероприятии Haowen Convocation Order, щелкните, чтобы просмотреть:Двойные заявки на внутреннюю и внешнюю стороны, призовой фонд в 20 000 юаней ждет вас, чтобы бросить вызов!"
Друзья, изучавшие React, знают, что в React своя система событий, которая полностью отличается от нативной системы событий DOM, но в чем разница, и в чем отличие и связь с нативным событием DOM? поскольку он включает в себя много кода, соответствующая логика кода не является связной, и общее понимание затруднено. Когда-то он был ошеломлен им. Позже, поняв основную логическую последовательность функций событий React, он смог разобраться во всей системе событий React. Сегодня давайте обсудим это.
Давайте сначала рассмотрим, как выглядит нативный поток событий DOM, а затем найдем источник вдохновения для событий React на основе делегирования нативных событий, затем мы обсудим, как работает система событий React, и разберем ее, чтобы понять коллекцию событий. и выполнение обратных вызовов. Шаг за шагом мы можем понять все события React Наконец, давайте подумаем о причинах и мотивах проектирования системы событий React и о том, какие факторы побудили команду React разработать этот путь. Структура написания и дизайн всей статьи также основаны на вышеизложенных идеях.Я надеюсь, что эта статья может вдохновить всех на новые мысли и идеи.
Весь код и исследования в этой статье основаны на 16.13.0.В следующей статье будет рассмотрена система событий в React 17. Добро пожаловать, чтобы следовать за мной
Собственный поток событий DOM
В браузере реализуем взаимодействие между JS и HTML через мониторинг событий. Страница часто связана со многими событиями, и последовательность событий, полученных страницей, представляет собой поток событий. Стандарт W3C предусматривает, что процесс распространения события проходит три этапа:
- этап захвата событий
- целевой этап
- фаза всплытия события
Давайте посмотрим на картинку
Когда событие запускается, оно сначала проходит через процесс захвата. Событие "проходит" от самого внешнего элемента и "переходит" к самому внутреннему элементу слой за слоем. Этот процесс будет продолжаться до тех пор, пока событие не достигнет своей цели. (элемент, который фактически инициирует событие); в это время течение времени переключается на «целевую фазу» (событие принимается целевым элементом); затем событие «отскакивает» и переходит в фазу бульканья, Дорога идет «вверх по течению» и идет обратно слой за слоем.
Это немного похоже на батут, который мы играем, прыгая на кровать, а затем отскочили, показывая «V» на протяжении всего процесса.
делегация мероприятия
В нативной DOM важным методом оптимизации производительности является делегирование событий, благодаря всплыванию событий передаются события дочерних узлов.e.target
захватить. См. код ниже:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>0</li>
</ul>
</body>
</html>
В этом коде, если вы хотите иметь возможность щелкнуть по каждому элементу li, вы можете вывести содержимое текста, что вам нужно сделать?
Самый прямой способ — привязать каждый элемент li к событию слушателя, которое записывается следующим образом:
<script>
// 获取 li 列表
var liList = document.getElementsByTagName('li')
// 逐个安装监听函数
for (var i = 0; i < liList.length; i++) {
liList[i].addEventListener('click', function (e) {。。
console.log(e.target.innerHTML)
})
}
</script>
Но это не элегантно, и производительность не очень хорошая.Если есть сотни или тысячи узлов, сотни или тысячи узлов будут прослушивать события.Как решить эту проблему? Мы можем использовать всплытие событий!
за эти 10li
элемент, независимо от того, где происходит щелчокli
, события кликов в конечном итоге будут переданы их общему отцу ---ul
элемент, поэтому мы можем позволитьul
чтобы помочь воспринять событие щелчка.
теперь, когдаul
может воспринимать события, то он может помочь обрабатывать события, потому что у нас естьe.target
,ul
Доступ к элементам можно получить через объект событияtarget
атрибут, получить элемент, который фактически запускает событие, и распределить логику обработки события для этого элемента, чтобы добиться реального «делегирования».
Согласно приведенной выше идее, мы можем избавиться от цикла for.Ниже приведен код, который использует прокси-сервер событий для достижения того же эффекта:
var ul = document.getElementById('poem')
ul.addEventListener('click', function(e){
console.log(e.target.innerHTML)
})
в кодеe.target
Атрибуты относятся к конкретной цели, которая инициирует событие, и записывают источник события. Так что здесь, независимо от того, на каком уровне выполняется функция слушателя, пока вы получаетеe.target
, то есть получить тот элемент, который действительно срабатывает.После получения элемента мы можем полностью смоделировать его поведение и добиться эффекта неразборчивого мониторинга.
Используя функцию всплытия событий, тот же тип логики мониторинга нескольких дочерних элементов объединяется с родительским элементом и управляется функцией слушателя, которая является делегированием событий.Благодаря делегированию событий можно сократить накладные расходы на память и упростить шаги регистрации. , Значительно повысить эффективность разработки.
Этот способ делегирования событий также является источником вдохновения React для синтеза событий. Далее давайте посмотрим, как работает система событий React.
Как работает система событий React
Система событий React использует идею делегирования событий. В React, за исключением нескольких специальных событий, не связанных с пузырьками (таких как события типа мультимедиа), которые не могут быть обработаны системой событий, большинство событий не привязаны к конкретным элементам, а единообразно привязаны к документу страницы, когда событие запускается на определенном узле DOM, оно в конечном итоге перейдет к документу, а унифицированный обработчик событий, привязанный к документу, распространит событие на конкретный экземпляр компонента.
Поэтому перед отправкой события React сначала оборачивает событие, оборачивая собственное событие DOM в синтетическое событие.
Что такое синтетические события React
Синтетические события — это настраиваемые объекты событий React, основанные наСпецификация W3CСинтетические события, определенные DOM, устраняют различия между различными браузерами на нижнем уровне и предоставляют разработчикам верхнего уровня унифицированный, стабильный и тот же интерфейс событий, что и собственные события DOM. Таким образом, разработчикам больше не нужно обращать внимание на громоздкие проблемы совместимости, а нужно сосредоточиться только на разработке бизнес-логики.
Хотя синтетические события не являются собственными событиями DOM, ссылки на собственные события DOM сохраняются. Если вам нужно получить доступ к собственному объекту события DOM, используйте синтетический объект события.e.nativeEvent
Атрибуты могут получать собственные ссылки на события DOM, как показано ниже:
e.nativeEvent
выходMouseEvent
Отсылки к родным событиям
Мы кратко рассказали об основных функциональных принципах системы событий React и имеем определенное представление об основных концепциях синтетических событий.Далее, в сочетании с исходным кодом React и стеком вызовов, мы будем иметь более глубокое понимание рабочий процесс системы событий.
Как работает система событий React
Система событий никогда не избежит двух основных действий "связывания событий" и "вызова событий". Давайте посмотрим, как реализована привязка событий.
привязка события
Привязка события завершается в процессе монтирования компонента (completeWork), в процессе монтирования будет три основных действия
- Создайте DOM-узел
- Вставьте узел DOM в дерево DOM
- Установить свойства узла DOM
Среди них ссылка для свойств узла настройки DOM будет проходить через PROPS узла FiberNode, что запускает ссылку регистрации события при обходе связанных с событием PROPS. На полный исходный код можно ссылатьсяЭтот адрес.
Учитывая, что исходный код может немного меняться со временем, я извлек код и удалил много мест, влияющих на понимание, код выглядит следующим образом:
function completeWork(current, workInProgress, renderLanes) {
// 取出 Fiber 节点的属性值,存储在 newProps 里
const newProps = workInProgress.pendingProps;
// 根据 workInProgress 节点的 tag 属性的不同,决定要进入哪段逻辑,
// 我们主要看 HostComponent,其他的都删掉了
switch (workInProgress.tag) {
// 这里有很多 case,但是为了方便理解,我们只保留了 HostComponent 的 case
case HostComponent: {
// Dom 节点的逻辑
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// 判断 current 节点是否存在,在挂载阶段,current 节点是不存在的,
// 这个逻辑只有在触发更新的时候会进入,等有时间我们再讲一下页面渲染和更新的逻辑
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 为 Dom 节点的创建做准备
const currentHostContext = getHostContext();
// 与服务端渲染有关的值 先不关注
const wasHydrated = popHydrationState(workInProgress);
// 判断是否是服务端渲染 先不关注
if (wasHydrated) {
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext
)
) {
markUpdate(workInProgress);
}
} else {
// 创建 DOM 节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress
);
// 尝试把上一步创建好的 Dom 节点挂载到 DOM 树上,
//这里需要注意下,有可能 DOM 树还不存在,但是没关系,看下面的一行代码
appendAllChildren(instance, workInProgress, false, false);
// stateNode 存储当前 Fiber 节点对应的 DOM 节点,
// 这里每个节点保存了自己的 DOM 实例,
// 所以即使是 DOM 树还不存在,也没关系,
//等 DOM 存在时候可以遍历的从 FiberNode 中取出 DOM 节点
workInProgress.stateNode = instance;
// 用来为 DOM 节点设置属性,这写法有点硬核,
//其实把返回的返回值赋值给一个变量,会好理解一些,不过源码里很多这种写法,难受
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
}
}
Из исходного кода общая блок-схема выглядит следующим образом:
Объединив исходный код и блок-схему, мы сосредоточимся на следующем:finalizeInitialChildren
, но это слишком неудобно, и я должен перейти кfinalizeInitialChildren
В исходном коде это полный набор кукол.Наша главная цель состоит в том, чтобы разобраться во всем процессе, вместо того, чтобы застрять в исходном коде и не в силах выбраться, и, наконец, позволить себе увидеть код и увидеть сомнения. жизни. Итак, сегодня я также делюсь здесь методом, позволяющим увидеть весь процесс вызова.
- использовать
create-react-app
Создайте проект реакции и замените его следующим кодом
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
return (
<div>
<div
onClick={(e) => {
console.log("原生的 DOM 事件是:", e.nativeEvent);
setCount(count + 1);
}}
>
<p>{count}</p>
</div>
</div>
);
}
export default App;
- Откройте панель производительности Chrome и нажмите кнопку «Обновить запись» (обведена).
- Подождите 3-4 секунды, затем прекратите запись, вы можете получить следующую большую картину стека вызовов.
- Затем мы находим
completeWork
, вы можете увидеть стек вызовов.
Наконец, по картинке всего стека вызовов мы можем реорганизовать нашу блок-схему, что намного проще, чем просмотр исходного кода.
Из рисунка также видно, что процесс регистрации события осуществляетсяensureListeningTo
функция включена, вensureListeningTo
, попытается получить корневой узел (объект документа) в текущей структуре DOM, а затем вызоветlegacyListenToEvent
, зарегистрируйте унифицированную функцию прослушивания событий в документе. Зарегистрируйте унифицированную функцию прослушивания событий в документе.
существуетlegacyListenToEvent
, на самом деле, позвонивlegacyListenToTopLevelEvent
Для управления отношениями между событиями и документами, отTopLevelEvent
Видно, что отслеживается событие верхнего уровня, и этот верхний уровень можно понимать как делегирование события верхнего уровня, то есть узел документа.
существуетlegacyListenToTopLevelEvent
Есть одно место, на которое стоит обратить внимание, я его обвел на картинке, а там естьlistenerMap
Переменная карты используется как структура данных хранения, в которой хранятся события всего документа.
это здесь,legacyListenToTopLevelEvent
Это отправная точка всего мероприятия, и мы сначала будем судитьlistenerMap
Есть ли в нем такое событие, но обратите внимание, здесьtopLevelType
Относится к контексту функции, представляющему тип события, например, если это событие щелчка, тоtopLevelType
Значение — клик. Как показано ниже:
Есть еще одна деталь, достойная нашего внимания, зачем использовать карту для хранения событий документа здесь, а также использоватьtopLevelType
Как ключ карты, подумайте хорошенько, это на самом деле оптимизация React,Даже если мы вызовем прослушиватель для одного и того же события несколько раз в проекте React, регистрация будет активирована только один раз для документа. И то, что React, наконец, регистрирует в документе, — это не конкретная логика обратного вызова на узле DOM, а просто функция отправки.
мы вaddEventBubbleListener
Поставьте точку останова и попробуйте, чтобы увидеть разницу!
На скриншоте мы видим, чтоelement
это документ,eventType
даclick
,а такжеlistener
то естьdispatchDiscreteEvent
Функция, хахаха, она такая прямолинейная, давайте посмотрим на нее дальшеdispatchDiscreteEvent
код в:
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent);
}
пройти черезdispatchDiscreteEvent
Из исходного кода мы можем видеть, что единая функция отправки событий, связанная с документом, на самом делеdispatchEvent
Последний вопрос: как в dispatchEvent реализовано распределение событий?
триггер события
Триггер события в принципе правильныйdispatchEvent
позвони, ноdispatchEvent
Триггерная ссылка относительно длинная, и у многих студентов должна закружиться голова из-за описанного выше процесса, поэтому я напрямую нарисовал весь основной рабочий процесс.
Первые три ступени уже были разобраны выше, нам странно 4,5,6 три ступени, три ступени это то, что мы должны фокусировать преодолевать.
Давайте разберемся в этом процессе через демонстрацию
import React, { useState } from "react";
function App() {
const [value, setValue] = useState(0);
return (
<div
onClickCapture={() => console.log("捕获经过 div")}
onClick={() => console.log("冒泡经过 div")}
>
<p>{value}</p>
<button
onClick={() => {
setValue(value + 1);
}}
>
加一
</button>
</div>
);
}
export default App;
Интерфейс, соответствующий этой демонстрации, выглядит следующим образом.
Демонстрация представляет собой строку числового текста и кнопку, каждый раз при нажатии на кнопку числовой текст будет +1. В структуре JSX, помимо кнопки, есть также тег div, который отслеживает события щелчка, этот тег div также прослушивает всплытие и захват события щелчка.
Структура дерева волокон, соответствующая всей демонстрации, выглядит следующим образом.
Мы понимаем процесс сбора обратных вызовов событий в соответствии с этой древовидной структурой Fiber.
function traverseTwoPhase(inst, fn, arg) {
// 定义一个 Path 数组,用来存储捕获和冒泡的节点
var path = [];
while (inst) {
// 把当前的节点收集进 path 数组
path.push(inst);
// 往上收集 tag === HostComponent 的父节点
inst = getParent(inst);
}
var i;
// 从后往前,收集 path 数组中参与捕获过程的节点与对应的回调
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
// 从前往后,收集 path 数组中参与冒泡过程的节点与对应回调
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
Из кода,traverseTwoPhase
Функция в основном делает следующие три вещи:
- Прокрутите коллекцию подходящих родительских узлов и сохраните их в массиве путей.
traverseTwoPhase
Он начнется с текущего узла (целевого узла, который запускает событие) и продолжит поиск вверх.tag === HostComponent
родительские узлы и собрать эти узлы по порядку в массив путей. Добавьте сюда описание, некоторые учащиеся могут не знать, почему они собирают толькоtag === HostComponent
,Это потому чтоHostComponent
Относится к соответствующему элементу DOMFiber
Тип узла. В браузере события DOM будут распространяться только между узлами DOM, и нет смысла собирать другие узлы.
Давайте взглянемpath
содержимое массива
Последний собранный контент — это сам узел кнопки (начальная точка этого цикла while, который будет перемещен в начало) и узел div.
- Моделируйте последовательность распространения событий на этапе захвата и собирайте экземпляры узлов и функции обратного вызова, связанные с этапом захвата.
traverseTwoPhase
Он будет проходить по массиву путей от конца к началу, имитировать последовательность захвата событий и собирать обратные вызовы и экземпляры, соответствующие событиям на этапе захвата.
Последний момент, о котором мы говорим, это то, что массив путей начинается с целевого узла и собирается вверх, поэтому дочерние узлы в массиве путей находятся впереди, а узлы-предки — сзади. этап узла DOM.
Обход массива путей сзади наперед на самом деле представляет собой процесс обхода дочерних узлов от родительского узла вниз до тех пор, пока не будет пройден целевой узел Порядок обхода такой же, как порядок, в котором события DOM распространяются в узле захвата. В течение всего процесса обхода функция fn будет поочередно проверять колбэки каждой ноды, и если на этой ноде есть соответствующее событие, экземпляр будет собран в синтетическое событие_dispatchInstances
(SyntheticEvent._dispatchInstances), обратный вызов события будет собран в_dispatchListeners
Свойства (SyntheticEvent._dispatchListeners) идут, ждут последующего исполнения. Мы наткнулись на точку останова при отладке вида:
Мы видим, что экземпляр div хранится в_dispatchInstances
, событие обратного вызова находится в_dispatchListeners
, ожидая последующего выполнения.
- Моделируйте порядок, в котором события распространяются на этапе всплытия, и собирайте соответствующие экземпляры узлов и функции обратного вызова на этапе всплытия.
После того, как работа этапа захвата завершена,traverseTwoPhase
Он будет проходить массив путей от начала к концу, имитировать всплывающую последовательность событий и собирать обратные вызовы и экземпляры, соответствующие событиям на этапе захвата.
Процесс в основном такой же, как шаг 2, разница в том, что порядок обхода массива путей изменяется с обратного порядка на положительный порядок. Обход прямого порядка также проверит ситуацию с обратным вызовом.Если всплывающий обратный вызов, соответствующий текущему времени на узле, не пуст, то экземпляр узла и обратный вызов события будут собираться отдельно.SyntheticEvent._dispatchInstances
а такжеSyntheticEvent._dispatchListeners
середина.
Следовательно, на этапе выполнения обратного вызова события нужно выполнить только для того, чтобыSyntheticEvent._dispatchListeners
Функция обратного вызова в массиве может имитировать весь полный поток событий DOM за один раз, что также «захват-цель-пузырь"три фазы.
Суммировать
Мы изучили рабочий процесс системы событий React на основе собственного потока событий DOM. Но есть вопрос, на который нет ответа: React имитирует поток событий DOM на основе синтетических событий. Этот вопрос часто задают в интервью, поэтому на данном этапе подведения итогов мы разберемся с этим вопросом.
- Синтетические события соответствуют спецификации W3C, сглаживая различия между различными браузерами на нижнем уровне и предоставляя разработчикам унифицированный и стабильный интерфейс событий на верхнем уровне, такой же, как собственные события DOM. Поэтому разработчики больше не могут обращать внимание на утомительные лежащие в основе проблемы совместимости и могут сосредоточиться на разработке бизнес-логики.
- Самостоятельно разработанная система событий позволяет React контролировать инициативу в обработке событий, точно так же, как уже существуют зрелые фреймворки, такие как React и Vue, но каждая крупная фабрика все еще разрабатывает свою собственную инфраструктуру для своих собственных бизнес-сценариев. React объединяет несколько событий в одно событие. Нативный DOM не справится с этим за него. Нативный упор делается на универсальность, в то время как React думает, что может сильно мешать производительности событий через собственную систему событий. потребности.
Весь код и исследования в этой статье основаны на 16.13.0.В следующей статье будет рассмотрена система событий в React 17. Добро пожаловать, чтобы следовать за мной