Статьи

Мастерство HTML5: контекст просмотра

HTML5 Mastery серия изображений

Когда мы создаем новую вкладку в браузере, создается новый контекст просмотра. Контекст просмотра объединяет множество различных вещей, которые мы считаем само собой разумеющимися: такие как управление сеансами и историей, навигация и загрузка ресурсов. У нас также есть цикл обработки событий, который используется для обработки взаимодействия между сценариями и DOM.

В этом уроке мы сначала получим общее представление о цикле событий, который в основном разработан по схеме реактора. Затем мы рассмотрим некоторые вспомогательные объекты, которые связаны с контекстом просмотра. Наконец, мы разработаем список лучших практик для максимально эффективного использования цикла обработки событий.

Каждый контекст просмотра поставляется со связанным циклом событий. Цикл событий привязан к window текущего документа контекста. Он может использоваться совместно с другим window котором размещается document того же происхождения.

Цикл обработки событий обеспечивает выполнение событий в нормализованном порядке, то есть так, чтобы они не могли чередоваться или выполняться параллельно. Например, хотя Worker позволяет нам запускать какой-то другой JavaScript параллельно, мы не можем напрямую взаимодействовать с DOM или основным JavaScript. Нам нужно отправлять сообщения, которые обрабатываются через события. Эти события обрабатываются циклом событий и поэтому будут выполняться в последовательном порядке.

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

  • У нас есть ресурсы (представленные в виде дескрипторов, то есть ссылок, таких как обратные вызовы JavaScript).
  • Демультиплексор для нормализации запуска событий.
  • Диспетчер, который регистрирует / отменяет регистрацию обработчиков и отправляет события.
  • Конечно, нам также нужны обработчики событий, чтобы использовать ресурсы, на которые ссылаются дескрипторы.

Демультиплексор отправляет дескриптор диспетчеру как можно скорее, то есть после обработки всех предыдущих событий. Внутри демультиплексор может иметь своего рода очередь, которая работает по принципу «первым пришел-первым вышел» (FIFO). Реальные реализации могут использовать какую-то очередь с приоритетами.

На следующей диаграмме показано соотношение между различными компонентами схемы реактора.

Образец реактора

В случае JavaScript, запущенного в браузере, дескриптор — это функция JavaScript, которая может захватывать некоторые объекты. Обработчики событий — это обработчики событий DOM. Реактор позволяет запускать код одновременно без каких-либо потенциальных проблем с многопоточностью.

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

Теперь, когда у нас есть смутное представление о схеме реактора и его реализации в цикле обработки событий, нам необходимо более подробно рассмотреть цикл обработки событий с точки зрения взаимодействия, которым в нашем случае является JavaScript.

Каждый раз, когда мы выполняем вызов функции в JavaScript, фрейм добавляется в так называемый стек вызовов во время выполнения. Стек имеет структуру «последний пришел — первым вышел» (LIFO). Стек вызовов отслеживает путь вызова. Фрейм состоит из аргументов функции и ее локального состояния, определяемых выполняемой в данный момент инструкции, и всех локальных переменных с соответствующими значениями.

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

1
2
3
4
5
function init() {
    setTimeout(function () {}, 0);
}
 
init();

Если мы теперь сохраняем стек вызовов после каждого изменения, мы получаем следующую картину. Начнем с пустого стека вызовов. После вызова функции init у нас есть элемент в стеке. Вызов setTimeout дает нам еще один элемент, даже если сама функция переключит контекст только на нативную функцию. Вернувшись к методу init мы остаемся с одним элементом в стеке. Наконец стек снова пуст.

Стек вызовов JavaScript

На этом этапе имеет смысл различать два режима запуска событий в JavaScript: синхронный и асинхронный. Разница может быть проиллюстрирована следующим примером кода. Мы используем асинхронный триггер события ( click ), чтобы запустить синхронный ( focus ). Обратите внимание, что click также может быть запущен синхронно с помощью метода click() .

01
02
03
04
05
06
07
08
09
10
11
12
var button = document.querySelector(‘button’);
var text = document.querySelector(‘input[type=text]’);
 
button.addEventListener(‘click’, function () {
    console.log(‘foo’);
    text.focus();
    console.log(‘bar’);
}, false);
  
text.addEventListener(‘focus’, function () {
    console.log(‘baz’);
}, false);

Мы увидим, что foo , baz и bar будут зарегистрированы. Поэтому событие focus было обработано сразу после вызова focus() . Есть также события, такие как события мутации DOM, которые всегда запускаются синхронно. Мы обсуждаем события мутации DOM в восьмой статье этой серии.

Реализация демультиплексора событий для цикла событий в браузере должна различать выполнение обычных обратных вызовов событий (известных как Задачи ) и предпочтительных меньших фрагментов кода, которые называются Микрозадачами .

В то время как задачи ставятся в очередь обычным способом, микрозадачи ставятся в очередь с высоким приоритетом. Они выполняются как можно быстрее, то есть всегда перед следующим заданием. Большинство взаимодействий DOM ставятся в очередь событий как задачи, например, вызывая функцию обратного вызова setTimeout . С другой стороны, обратные вызовы нового типа Promise вызываются как микрозадачи.

Другим примером API, который ставит в очередь микрозадачи, является интерфейс MutationObserver . На данный момент мы отложим обсуждение MutationObserver до восьмой статьи этой серии.

Следующий пример иллюстрирует разницу между обычной задачей и микрозадачей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
function init () {
    console.log(‘foo’);
    setTimeout(later, 0);
    now();
    Promise.resolve().then(soon);
    console.log(‘bar’);
}
 
function later () {
    console.log(‘baz’);
}
 
function soon () {
    console.log(‘norf’);
}
 
function now () {
    console.log(‘qux’);
}
 
init();

Мы увидим, что foo , qux , bar , norf и baz зарегистрированы. Порядок norf и baz может быть неправильным в зависимости от браузера, но должен быть таким, как представлено. Запуск предыдущего примера кода в консоли браузера также дает интересный результат.

В качестве примера, результат для последней версии Opera отображается ниже.

Задача MicroTask Promise Opera

Результат функции init ( undefined ) отображается после выполнения обратного вызова из Promise . Поэтому средства отладки браузера интегрируются с использованием обычной задачи, а не микротрудника.

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

Контекст просмотра содержит много других сервисов. Большинство этих служб обычно недоступны, но некоторые из них могут предоставлять API, который мы можем использовать. Например, некоторые аппаратные службы отображаются в объекте navigator .

Кроме того, мы можем получить доступ к (локальной) истории через объект history . Здесь мы находим несколько полезных методов. Предположительно наиболее полезным является pushState() . Требуется три параметра:

  1. Состояние записи, чтобы нажать. Это должна быть строка. Мы могли бы использовать структуру данных JSON.
  2. Название для нашей справки. Это не используется большинством браузеров, поэтому мы можем вообще опустить этот параметр.
  3. Наконец URL для отображения. Браузеры обычно показывают это в строке адреса.

Давайте посмотрим на пример.

1
history.pushState(null, null, ‘http://www.example.com/my-state’);

Вызов pushState немедленно изменит отображаемый URL. Что происходит, когда пользователь нажимает на вездесущую кнопку «Назад»? Вообще говоря, это зависит от браузера, но хорошим решением было бы всплыть текущее состояние.

Операции вперед или назад также отображаются через объект history . Например, мы можем сделать следующее:

1
2
history.back();
history.forward();

Обратный вызов программно или через пользовательский интерфейс может привести к возврату на предыдущую страницу. Если стек уже пуст, мы не можем вытолкнуть текущее состояние. Это также можно увидеть в короткой демонстрации, которая включает две кнопки и список.

Пример кода для запуска логики выглядит следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
var list = document.querySelector(‘ul’);
var buttons = document.querySelectorAll(‘button’);
var back = buttons[0];
var forward = buttons[1];
 
back.addEventListener(‘click’, function (ev) {
  history.back();
}, false);
 
forward.addEventListener(‘click’, function (ev) {
  var c = list.childElementCount.toString();
  var url = ‘foo-‘ + c;
  history.pushState(c, null, url);
  var item = document.createElement(‘li’);
  item.textContent = url;
  list.appendChild(item);
}, false);
 
window.addEventListener(‘popstate’, function (ev) {
  list.removeChild(list.children[ev.state * 1]);
}, false);

API истории — это хороший способ реализовать маршрутизацию в одностраничном приложении (SPA). Альтернативный способ, который все еще использует URL для маршрутизации, дается путем манипулирования хешем URL и прослушивания события hashchange . Мы снова используем события для асинхронного запуска обратных вызовов.

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

Простая схема отображения цикла событий показана ниже. Цикл вращается свободно, пока задача не будет запущена. Если мы считаем задачу связанной с JavaScript, нам нужно передать управление движку JavaScript. Наконец, код может зарегистрировать еще несколько обратных вызовов. Если событие вызывается, все обратные вызовы помещаются в цикл событий как задачи, которые необходимо выполнить.

Loop Event JavaScript

В браузерах обратные вызовы ставятся в очередь каждый раз, когда происходит соответствующее событие. События без обратного вызова ничего не ставят в очередь.

Вызов setTimeout() ожидает, по крайней мере, предоставленное время, прежде чем ставить в очередь указанный обратный вызов в очереди. Если в очереди нет других задач, обратный вызов будет вызван сразу, в противном случае нам придется ждать. По этой причине второй аргумент setTimeout определяет минимальное время, а не гарантированное время.

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

В общем, мы всегда должны стараться избегать использования кода блокировки. Большая часть API уже представлена ​​неблокирующим образом, либо с использованием обратных вызовов, либо событий. К сожалению, существуют устаревшие исключения, такие как alert или синхронный XHR. Они никогда не должны использоваться, если мы не знаем точно, что мы делаем.

Так стоит ли нам использовать задачу или микрозадачу? Promise API использует микрозадачу по определенной причине. Если возможно, мы можем стремиться к этому. Микрозадача выполняется как можно быстрее, практически следуя сразу после выполнения текущего кода. Это может предотвратить ненужный рендеринг. Зачем нам делать рендеринг один раз перед вставкой результата, что приведет к необходимости еще одной операции рендеринга? Однако, если вы сомневаетесь, мы должны передать управление обратно в браузер, используя стандартную задачу.

Важно полностью понимать, как работает JavaScript и как он взаимодействует с DOM и другими ресурсами. Это начинается с цикла событий. Концепция не только реализована в браузере, но также присутствует в ядре Node.js. Овладение JavaScript означает овладение циклом событий.

В предыдущем уроке мы видели, что браузер предоставляет нам несколько механизмов, которые объединяются в контексте просмотра и раскрываются в нескольких точках. Хотя мы не можем контролировать некоторые внутренние механизмы, мы все равно должны знать, что они существуют и как они работают.