Как понимать события React 16

внешний интерфейс React.js

如何理解 React.16 的事件系统

«Эта статья участвовала в мероприятии 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» на протяжении всего процесса.

事件流就像是这个蹦床,跳下去,弹起来,呈现一个 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, как показано ниже:

通过合成事件拿到原生 DOM 的引用

e.nativeEventвыходMouseEventОтсылки к родным событиям

打印出来的原生 DOM 的引用

Мы кратко рассказали об основных функциональных принципах системы событий 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;
    }
  }
}

Из исходного кода общая блок-схема выглядит следующим образом:

completeWork 内大致流程图

Объединив исходный код и блок-схему, мы сосредоточимся на следующем:finalizeInitialChildren, но это слишком неудобно, и я должен перейти кfinalizeInitialChildrenВ исходном коде это полный набор кукол.Наша главная цель состоит в том, чтобы разобраться во всем процессе, вместо того, чтобы застрять в исходном коде и не в силах выбраться, и, наконец, позволить себе увидеть код и увидеть сомнения. жизни. Итак, сегодня я также делюсь здесь методом, позволяющим увидеть весь процесс вызова.

  1. использовать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;
  1. Откройте панель производительности Chrome и нажмите кнопку «Обновить запись» (обведена).

Performance 面板

  1. Подождите 3-4 секунды, затем прекратите запись, вы можете получить следующую большую картину стека вызовов.

调用栈大图

  1. Затем мы находимcompleteWork, вы можете увидеть стек вызовов.

completeWork 调用栈

Наконец, по картинке всего стека вызовов мы можем реорганизовать нашу блок-схему, что намного проще, чем просмотр исходного кода.

image

Из рисунка также видно, что процесс регистрации события осуществляетсяensureListeningToфункция включена, вensureListeningTo, попытается получить корневой узел (объект документа) в текущей структуре DOM, а затем вызоветlegacyListenToEvent, зарегистрируйте унифицированную функцию прослушивания событий в документе. Зарегистрируйте унифицированную функцию прослушивания событий в документе.

существуетlegacyListenToEvent, на самом деле, позвонивlegacyListenToTopLevelEventДля управления отношениями между событиями и документами, отTopLevelEventВидно, что отслеживается событие верхнего уровня, и этот верхний уровень можно понимать как делегирование события верхнего уровня, то есть узел документа.

существуетlegacyListenToTopLevelEventЕсть одно место, на которое стоит обратить внимание, я его обвел на картинке, а там естьlistenerMapПеременная карты используется как структура данных хранения, в которой хранятся события всего документа.

legacyListenToTopLevelEvent

это здесь,legacyListenToTopLevelEventЭто отправная точка всего мероприятия, и мы сначала будем судитьlistenerMapЕсть ли в нем такое событие, но обратите внимание, здесьtopLevelTypeОтносится к контексту функции, представляющему тип события, например, если это событие щелчка, тоtopLevelTypeЗначение — клик. Как показано ниже:

Demo 中打断点出来的 topLevelType

Есть еще одна деталь, достойная нашего внимания, зачем использовать карту для хранения событий документа здесь, а также использоватьtopLevelTypeКак ключ карты, подумайте хорошенько, это на самом деле оптимизация React,Даже если мы вызовем прослушиватель для одного и того же события несколько раз в проекте React, регистрация будет активирована только один раз для документа. И то, что React, наконец, регистрирует в документе, — это не конкретная логика обратного вызова на узле DOM, а просто функция отправки.

мы вaddEventBubbleListenerПоставьте точку останова и попробуйте, чтобы увидеть разницу!

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Триггерная ссылка относительно длинная, и у многих студентов должна закружиться голова из-за описанного выше процесса, поэтому я напрямую нарисовал весь основной рабочий процесс.

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;

Интерфейс, соответствующий этой демонстрации, выглядит следующим образом.

Demo 对应的界面

Демонстрация представляет собой строку числового текста и кнопку, каждый раз при нажатии на кнопку числовой текст будет +1. В структуре JSX, помимо кнопки, есть также тег div, который отслеживает события щелчка, этот тег div также прослушивает всплытие и захват события щелчка.

Структура дерева волокон, соответствующая всей демонстрации, выглядит следующим образом.

Fiber 树

Мы понимаем процесс сбора обратных вызовов событий в соответствии с этой древовидной структурой 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Функция в основном делает следующие три вещи:

  1. Прокрутите коллекцию подходящих родительских узлов и сохраните их в массиве путей.

traverseTwoPhaseОн начнется с текущего узла (целевого узла, который запускает событие) и продолжит поиск вверх.tag === HostComponentродительские узлы и собрать эти узлы по порядку в массив путей. Добавьте сюда описание, некоторые учащиеся могут не знать, почему они собирают толькоtag === HostComponent,Это потому чтоHostComponentОтносится к соответствующему элементу DOMFiberТип узла. В браузере события DOM будут распространяться только между узлами DOM, и нет смысла собирать другие узлы.

Давайте взглянемpathсодержимое массива

path 数组的内容

Последний собранный контент — это сам узел кнопки (начальная точка этого цикла while, который будет перемещен в начало) и узел div.

  1. Моделируйте последовательность распространения событий на этапе захвата и собирайте экземпляры узлов и функции обратного вызова, связанные с этапом захвата.

traverseTwoPhaseОн будет проходить по массиву путей от конца к началу, имитировать последовательность захвата событий и собирать обратные вызовы и экземпляры, соответствующие событиям на этапе захвата.

Последний момент, о котором мы говорим, это то, что массив путей начинается с целевого узла и собирается вверх, поэтому дочерние узлы в массиве путей находятся впереди, а узлы-предки — сзади. этап узла DOM.

Обход массива путей сзади наперед на самом деле представляет собой процесс обхода дочерних узлов от родительского узла вниз до тех пор, пока не будет пройден целевой узел Порядок обхода такой же, как порядок, в котором события DOM распространяются в узле захвата. В течение всего процесса обхода функция fn будет поочередно проверять колбэки каждой ноды, и если на этой ноде есть соответствующее событие, экземпляр будет собран в синтетическое событие_dispatchInstances(SyntheticEvent._dispatchInstances), обратный вызов события будет собран в_dispatchListenersСвойства (SyntheticEvent._dispatchListeners) идут, ждут последующего исполнения. Мы наткнулись на точку останова при отладке вида:

image

Мы видим, что экземпляр div хранится в_dispatchInstances, событие обратного вызова находится в_dispatchListeners, ожидая последующего выполнения.

  1. Моделируйте порядок, в котором события распространяются на этапе всплытия, и собирайте соответствующие экземпляры узлов и функции обратного вызова на этапе всплытия.

После того, как работа этапа захвата завершена,traverseTwoPhaseОн будет проходить массив путей от начала к концу, имитировать всплывающую последовательность событий и собирать обратные вызовы и экземпляры, соответствующие событиям на этапе захвата.

Процесс в основном такой же, как шаг 2, разница в том, что порядок обхода массива путей изменяется с обратного порядка на положительный порядок. Обход прямого порядка также проверит ситуацию с обратным вызовом.Если всплывающий обратный вызов, соответствующий текущему времени на узле, не пуст, то экземпляр узла и обратный вызов события будут собираться отдельно.SyntheticEvent._dispatchInstancesа такжеSyntheticEvent._dispatchListenersсередина.

Следовательно, на этапе выполнения обратного вызова события нужно выполнить только для того, чтобыSyntheticEvent._dispatchListenersФункция обратного вызова в массиве может имитировать весь полный поток событий DOM за один раз, что также «захват-цель-пузырь"три фазы.

Суммировать

Мы изучили рабочий процесс системы событий React на основе собственного потока событий DOM. Но есть вопрос, на который нет ответа: React имитирует поток событий DOM на основе синтетических событий. Этот вопрос часто задают в интервью, поэтому на данном этапе подведения итогов мы разберемся с этим вопросом.

  1. Синтетические события соответствуют спецификации W3C, сглаживая различия между различными браузерами на нижнем уровне и предоставляя разработчикам унифицированный и стабильный интерфейс событий на верхнем уровне, такой же, как собственные события DOM. Поэтому разработчики больше не могут обращать внимание на утомительные лежащие в основе проблемы совместимости и могут сосредоточиться на разработке бизнес-логики.
  2. Самостоятельно разработанная система событий позволяет React контролировать инициативу в обработке событий, точно так же, как уже существуют зрелые фреймворки, такие как React и Vue, но каждая крупная фабрика все еще разрабатывает свою собственную инфраструктуру для своих собственных бизнес-сценариев. React объединяет несколько событий в одно событие. Нативный DOM не справится с этим за него. Нативный упор делается на универсальность, в то время как React думает, что может сильно мешать производительности событий через собственную систему событий. потребности.

Весь код и исследования в этой статье основаны на 16.13.0.В следующей статье будет рассмотрена система событий в React 17. Добро пожаловать, чтобы следовать за мной