Расскажу я вам одну историю. Как-то раз я разрабатывал очередной компонент с выбором даты для нашей системы проектирования. Компонент состоит из поля для текстового ввода и всплывающего календаря, отображаемого при щелчке мышью по этому полю. Затем всплывший календарь можно закрыть, щелкнув вне него или в случае, если была выбрана дата.
Компонент для выбора даты
В большинстве реализаций такой логики «щелкнуть вне календаря» применялись конкретные слушатели событий, прикрепленные к DOM. Но я хотел сделать наш компонент с выбором даты доступным, так, чтобы можно было открывать календарь с вкладками и таким же образом его закрывать. Кроме того, слушатели событий могут вступать в конфликт друг с другом, если вы разместите на странице несколько таких компонентов для выбора даты.
А что, если просто положиться на нативные события фокусировки и размытия, а не связываться с щелчками вне поля? В таком случае, естественно, поддерживаются вкладки, события касания и щелчка, и все это уже реализовано в браузере. Единственная проблема, которую придется решить в данном случае, такова: что делать, если вы щелкаете по всплывающему полю, но без выбора даты. Тогда фокус перемещается на календарь, в результате с полем для ввода происходит размытие, и, в конце концов, всплывающее поле скрывается.
Здесь я задумался, а есть ли способ совершить щелчок, но не сдвигать фокус. Наскоро погуглив, я нашел, как это сделать: подавить событие mouseDown, по умолчанию срабатывающее для всплывающего поля. Все так: одной строки хватило, чтобы все щелчки работали, но поле для ввода текста оставалось в фокусе.
Казалось: вот и все, решение найдено. Давайте двигаться дальше. Но что-то меня останавливало. Почему именно mouseDown, а не mouseUp останавливает фокус, но пропускает щелчок? Это часть какого-то действующего стандарта? Будет ли это работать в кроссбраузерном режиме? Библиотека React Testing, при помощи которой у нас делались интеграционные тесты, также не поддерживала эту возможность, поэтому мне пришлось бы менять функцию симуляции.
❯ Что такое веб-стандарт?
Хорошо, поскольку ответа со Stack Overflow мне было недостаточно, то где еще искать информацию о поведении веб-браузеров, как не в вею-стандартах?
Вероятно, вы слышали о W3C, он же – Консорциум Всемирной Паутины. Это международное сообщество, разрабатывающее открытые стандарты для Веба. W3C стремится гарантировать, что все руководствуются одними и теми же нормами, и нам не придется поддерживать десятки совершенно разных окружений. Если зайдете на их сайт, то найдете список всех стандартов, над которыми они работают.
Давайте заглянем в один документ, где могут быть ответы на наши вопросы - UI Events Standard (Стандарт событий пользовательского интерфейса). В этом документе указан поток событий DOM, определен список событий и порядок их выполнения. Если вам казалось, что веб-стандарты – это скучные, мутные простыни текста, через которые приходится продираться – сразу переходите к разделу DOM Event Architecture (Архитектура событий DOM). Здесь объяснено всплытие событий, рассказано о захвате событий, а сам текст снабжен веселыми картинками. Тем не менее, это очень конкретный документ, именно такой, каким и должен быть стандарт. Вас удивит его качество, он на самом деле очень качественно написан, изобилует примерами и рекомендациями.
Также в нем определено и наше событие mouseDown, в частности, как оно действует по умолчанию:
“Во многих реализациях событие mousedown используется, чтобы инициировать ряд контекстно-зависимых действий, выполняемых по умолчанию. Такие действия можно предотвратить, если данное событие отменено. В число таких действий, выполняемых по умолчанию, могут входить: начало перетаскивания, где перетаскиваемый объект – это изображение или ссылка; начало выделения текста, т. д. Кроме того, в некоторых реализациях предусматривается возможность панорамирования при помощи мыши, активируемое, когда средняя кнопка мыши утоплена в момент диспетчеризации события mousedown.”
Хорошо, для нашего события предусмотрены некоторые действия по умолчанию, но в самом событии фокуса нет ничего специфического, поскольку фокус действительно зависит от реализаций браузеров. Давайте с ними познакомимся.
❯ Знакомство с браузерными движками
Современный браузер – это весьма сложный образец софта с базой кода примерно в десятки миллионов строк. Поэтому обычно браузер делится на несколько частей.
Чтобы найти, где именно определяются события фокуса, требуется сделать обзор, который позволил бы понять, за что отвечает каждая часть. Начнем с Chromium и документации по его проектированию Getting Around The Chrome Source Code. Как видите, здесь множество модулей, и логика, за которую отвечают модули, у всех модулей разная.
Обобщенный обзор Chromium
Давайте кратко разберем их все, чтобы понять, как взаимодействуют все эти компоненты.
- chrome: это базовое приложение с логикой запуска, пользовательским интерфейсом и всеми окнами. Он содержит проекты для chrome.exe и chrome.dll. Здесь вы также найдете ресурсы, например, иконки или курсоры.
- content: это серверная часть приложения, обрабатывающая коммуникацию с дочерними процессами.
- net: это сетевая библиотека, помогающая выполнять запросы к веб-сайтам.
- base: это место для общего кода, разделяемого между всеми субпроектами. Сюда могут быть включены такие вещи как операции над строками, обобщенные утилиты, т. д.
- blink: это движок рендеринга, отвечающий за весь конвейер отображения, в том числе, за деревья DOM, стили, события, интеграцию с V8.
- v8: последняя большая часть браузерного движка на JavaScript. Задача этого компонента – компилировать JavaScript в нативный машинный код.
Как видите, браузер состоит из нескольких независимых частей, общающихся друг с другом через API. С точки зрения разработчика обычно наиболее интересны Blink и V8. Действия по умолчанию, определяемые браузером, не входят в состав V8, но в Blink все они должны быть определены и реализованы. Но, прежде, чем переходить к базе кода Blink, давайте разберемся, как веб-браузеры работают с точки зрения пользователя.
❯ Конвейер рендеринга
Представьте, что вы вводите в браузер адрес домена, и затем браузер выбирает и загружает набор ресурсов: HTML, CSS, файлы JS, изображения, ярлыки. Но что должно произойти далее?
Браузерный конвейер рендеринга
На первом шаге HTML-файлы будут проходить синтаксический разбор и преобразовываться в дерево DOM. DOM – это не только внутреннее представление страницы, но и API, открываемый коду JavaScript для выполнения запросов и модификации рендеринга при помощи системы так называемых «привязок».
Следующий шаг после построения дерева DOM – это обработка стилей CSS. Для этой цели в браузерах есть инструмент разбора CSS, собирающий модель стилевых правил. Построив модель стилевых правил, можно объединить их с набором стилей, задаваемых по умолчанию (предоставляемых браузером) и вычислить окончательное значение каждого стилевого свойства для каждого элемента DOM. Этот процесс называется разрешением (или пересчетом) стилей.
В следующей части, описывающей макет, требуется определить визуальную геометрию для всех элементов. На данном этапе каждый элемент получает координаты (x и y), ширину и высоту. Движок компоновки вычисляет все зоны выхода за границы и ведет их учет – какая часть элемента будет видимой, а какая нет.
Когда у нас будут все координаты для всех элементов, придет время для отрисовки. Для этой операции мы воспользуемся координатами с предыдущего шага и цветами из стилевых правил, после чего скомбинируем на их основе список инструкций по отрисовке. Важно рисовать элементы в правильном порядке, так, чтобы, накладываясь друг на друга, они сохраняли правильную «этажность». Этот порядок можно изменить при помощи стилевого правила z-index.
Давайте выполним наши отрисовочные инструкции по списку и преобразуем их в растр цветовых значений. Этот этап называется растеризацией. Именно сейчас мы возьмем наши изображения и декодируем их в карту битов.
Позже растеризованная карта битов будет храниться в памяти GPU. На данном этапе в работу включаются библиотеки, абстрагирующие аппаратное обеспечение и совершающие вызовы к OpenGL и DirectX под Windows. Когда GPU получает команды отображать растровый рисунок, на дисплее отрисовываются соответствующие пиксели.
Вот у нас и есть все важнейшие части рендерингового конвейера. Но что произойдет, если прокрутить страницу, либо применить на ней некоторую анимацию? На самом деле, рендеринг не статичен. Для представления изменений используются анимационные кадры. Каждый кадр полностью отображает состояние контента в конкретный момент времени. Самое сложное при организации этого процесса – добиться нужной производительности. Чтобы анимация шла гладко, необходимо генерировать, как минимум, 60 кадров в секунду. Провернуть весь конвейер 60 раз в секунду будет практически невозможно, в особенности, на медленных устройствах.
А что, если мы не будем раз за разом рендерить все заново, а предоставим способ инвалидировать элемент на конкретном этапе. Скажем, если вы динамически меняете цвет кнопки, то браузер пометит ее узел как инвалидированный, и в следующем кадре анимации она будет отображена заново. Если ничего не изменится, то мы сможем повторно использовать старый кадр.
Этот способ удобен для оптимизации небольших динамических изменений содержимого. Давайте подумаем, что делать, если требуется менять большие области. Например, после полной прокрутки страницы все пиксели на новом экране будут иными. Поэтому страница декомпонуется на уровни, каждый из которых растрируется независимо. Уровень может быть очень мал и представлять всего один узел a DOM. Затем все эти уровни будут комбинироваться в другом потоке, который называется поток композитора. При такой оптимизации не приходится повторно растрировать все сразу, достаточно выполнять эти операции для небольших слоев, а затем правильно компоновать их друг с другом.
Итак, мы вкратце рассмотрели, что делает Blink, и как выглядит конвейер рендеринга. Давайте углубимся в код.
❯ Навигация по базе кода Blink
Кажется, мы приближаемся к финишной прямой. Давайте откроем репозиторий Blink и осмотримся в нем.
Корневой каталог репозитория Blink
Быстро становится ясно, что, хотя мы и смогли значительно конкретизировать наш исходный вопрос, массив кода пока все равно слишком велик, чтобы вручную найти в нем конкретную строку, отвечающую за предотвращение фокуса.
Давайте попробуем поискать в Google по имени события:
mousedown site:https://chromium.googlesource.com/chromium/blink/+/master/Source
Приводит нас к файлу EventHandler, где можно найти детали реализации для множества входных событий. В том числе, ту строку, которая нас наиболее интересует.
bool swallowEvent = !dispatchMouseEvent(EventTypeNames::mousedown, mev.innerNode(), m_clickCount, mouseEvent);
Возвращаемое значение
dispatchMouseEvent
означает «продолжать обрабатывать так, как задано по умолчанию», поэтому, в случае использования preventDefault
, swallowEvent
равно true
.Чуть ниже расположен вызов события фокусировки, срабатывающий лишь в случае, если
swallowEvent == false
.swallowEvent = swallowEvent || handleMouseFocus(MouseEventWithHitTestResults(mouseEvent, hitTestResult), sourceCapabilities);
Можете исследовать не только событие фокусировки, но и все действия, задаваемые по умолчанию для события mouse down, в том числе, выделение, перетаскивание и работу с ползунком. Также здесь реализуются событие отпускания кнопки мыши и событие двойного щелчка.
❯ Gecko и WebKit
Добравшись сюда, мы уже успели немало покопаться в исходном коде браузеров и очень неплохо представляем себе их структуру. Так почему бы не проверить Firefox или вообще Safari. Движок браузера Firefox называется Gecko, а движок Safari - WebKit.
В Gecko также предусмотрена обзорная страница для разработчиков, так что легко уяснить основные концепции этого движка. Опираясь на опыт работы с Chrome, вы найдете здесь аккуратный файл EventStateManager на 6000 строк кода, в котором для событий задаются действия и поведения по умолчанию.
WebKit – это браузерный движок от Apple, используемый в Safari и других продуктах Apple. Blink от Chrome является форком WebKit, поэтому у них много общего, и мне не составило труда найти реализации событий в их версии файла EventHandler.
Теперь, убедившись, что можно безопасно подавить событие mousedown, я могу вернуться к началу этого поста и спокойно доработать инструмент для выбора даты.
❯ Заключение
Вместе мы проделали путь пот простой проблемы к введению в веб-стандарты и разбору деталей, касающихся реализации браузеров.
Пусть вас не пугает скрытая сложность имеющихся модулей, даже относящихся к браузеру или компилятору. Вас ждет увлекательное путешествие. Вероятно, вы легко найдете, что здесь можно было бы улучшить, и, что гораздо важнее, на собственном опыте приобретете уникальные знания о том, как именно что устроено. Лично я перелопатил гору документации, работая над этим исследованием, и рекомендую всем действовать так же. Во всех браузерах предоставляется отличная документация, поэтому я в самом деле не уверен, а нужно ли для работы еще что-то кроме нее.