Цикл событий JavaScript

  1. Зачем нам нужно асинхронное поведение в браузере
  2. Мужество: стек вызовов и очередь сообщений
  3. Асинхронный JavaScript
  4. SetTimeout (..., 0)
  5. Область обратного вызова
  6. Регистрация слушателей
  7. Обзор кода
  8. Завершение

У вас был шанс поиграть в DOM, и он позволяет вам делать довольно интересные вещи. Но вы действительно увидите всю мощь jQuery, когда узнаете о событиях в браузере. Если манипулирование DOM - это хлеб с маслом, события - это торт.

Ваш браузер - это один большой бесконечный цикл. Он ждет, чтобы вы все время что-то делали, и когда вы это делаете, он запускает «События», чтобы описать то, что вы только что сделали. Некоторые события генерируются пользователем, такие как щелчок, зависание, перемещение мыши, ввод клавиш и т. Д. Другие генерируются браузером или жизненным циклом различных запросов. Вы уже работали с одним из них - функция $ (document) .ready () прослушивает событие ready, которое ваш браузер отправляет после завершения загрузки всего DOM.

Ваш код JavaScript может зарегистрировать «слушатели» для тех событий, которые являются функциями, которые вызываются при обнаружении события.

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

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

Зачем нам нужно асинхронное поведение в браузере

Чтобы понять, как обрабатываются события, стоит сделать шаг назад и посмотреть, как работает собственный цикл событий JavaScript. Это функция, которая дает JavaScript возможность действовать асинхронно и является определяющей характеристикой, которая делает его привлекательным языком для других приложений, таких как программирование на стороне сервера (через Node.js).

Такое асинхронное поведение необходимо, потому что типы событий, с которыми работает JavaScript, часто бывают трудоемкими (HTTP-запросы) или сильно прерывистыми (щелчки мыши). Ваш браузер буквально бомбардируется различными событиями все время. Если бы каждое из этих событий блокировало выполнение других функций, у вас были бы проблемы.

Допустим, вы нажимаете кнопку, и она отправляет сетевой запрос, выполнение которого занимает много времени. Если бы процесс был действительно однопоточным и синхронным, кнопка даже не смогла бы перерисовать себя в поднятом положении как «не нажато», пока запрос не вернется.

Мы еще немного посмотрим, сколько времени займет выполнение определенных операций за минуту. Однако сначала давайте поднимем капот и посмотрим, что на самом деле происходит с JavaScript.

Мужество: стек вызовов и очередь сообщений

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

Стек вызовов содержит список сообщений (вызовов методов), которые должны быть выполнены ПРЯМО СЕЙЧАС. Каждое из этих сообщений называется стековым фреймом, когда оно находится в стеке вызовов. Новый фрейм создается каждый раз, когда среда выполнения сталкивается с вызовом метода или любым другим поведением в сценарии, таким как запуск события. Если выполняемый метод вызывает другой метод, это новое сообщение добавляется в верхнюю часть стека и теперь имеет полный приоритет. Когда сообщение возвращается (с или без возвращаемого значения), его кадр удаляется из стека.

Среда выполнения постоянно проходит по стеку вызовов и выполняет кадры сверху вниз. Поскольку JavaScript является однопоточным, выполнение фрейма требует полного внимания. Но это происходит очень быстро, поэтому он часто работает через стек и достигает дна.

Несколько реальных точек интереса о стеке:

  1. Вы также должны теперь иметь возможность визуализировать, что происходит во время выполнения программы - и как наличие методов, вызывающих методы, вызывающие методы, будет просто поддерживать рост стека до тех пор, пока не вернется самый глубокий уровень, а затем стек постепенно сжимается вниз, пока каждый последующий метод не возвращается до оно исчезает, когда возвращается последнее сообщение.
  2. Теперь вы можете видеть, как создание бесконечного цикла, в котором функция продолжает вызывать себя, может вызвать бесконечный рост стека ... и создать условие "переполнения стека".

Другие популярные языки на самом деле используют только стек вызовов - когда интерпретатор просто меняет все в линейном порядке. Это добавление очереди, которая позволяет JavaScript начинать делать интересные вещи.

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

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

Асинхронный JavaScript

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

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

  1. L1-кэш: 3 цикла
  2. L2-кеш: 14 циклов
  3. RAM: 250 циклов
  4. Диск: 41 000 000 циклов
  5. Сеть: 240 000 000 циклов

Это верно - выполнение сетевого запроса в миллион раз медленнее, чем доступ к ОЗУ. Но все это занимает ограниченное количество времени. Если вы ожидаете завершения сетевого запроса и имеете только стек вызовов для работы, ваше приложение в основном просто переходит в спящий режим, пока не вернется.

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

Например, если вы выполняете сетевой запрос «XHR» (AJAX) в браузере, JavaScript разгрузит свой обратный вызов в отдельную очередь, которая обрабатывается браузером. Когда запрос завершается, браузер обнаруживает это и помещает обратный вызов в очередь JavaScript для нормальной работы. В то же время, для вашего JavaScript-кода жизнь продолжается как обычно, и он может продолжать обрабатывать стек и очередь в обычном режиме.

Другие типы ввода-вывода могут использовать другие очереди, которые обычно предоставляются вашим веб-браузером.

SetTimeout (..., 0)

Иногда есть хитрость, которую разработчики JavaScript используют для асинхронного запуска метода. Для этого передайте метод в качестве обратного вызова функции setTimeout, которая задерживает выполнение на указанный интервал времени. Функция обратного вызова передается API синхронизации вашего браузера и отправляется обратно после указанной задержки.

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

Хотя технических деталей может быть больше, чем вам нужно знать, полезно сделать функцию неблокируемой:

console.log («Я был казнен первым!»); // обратите внимание на задержку 0 мс. Вы можете экспериментировать // с передачей этих разных значений. // 1000ms = 1s setTimeout (function () {console.log («Я сейчас в конце очереди!»);}, 0); console.log («Я исполняюсь раньше»); console.log («JavaScript проверяется»); console.log («любые новые сообщения в очереди»); console.log («так что myNonBlockingFunction не будет»); console.log («беги, пока правильно ...»); console.log ( "о ..."); console.log ( "Now!");

См Визуализация этого примера здесь.

Если вам интересно, см. Вкладку «Ресурсы» для дополнительного чтения и объяснения видео.

Область обратного вызова

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

Это вопросы Scope. На практике, стек вызовов и переменные среды, необходимые для обратного вызова, все хорошо упакованы в замыкание и сохраняются, когда они необходимы. Опять же, мы поговорим об этом чуть позже.

Для хорошего объяснения всего этого см. этот блог из углеродной пятерки.

Регистрация слушателей

Если вы думаете о цикле событий JavaScript в контексте браузера, все становится более интересным.

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

Старый способ добавления прослушивателей событий заключался в том, чтобы помещать их непосредственно в разметку HTML, добавляя свойства, такие как onclick, которые могли бы выполнять код немедленно или вызывать другие функции JavaScript. Это НЕ считается хорошей практикой, потому что оно тесно связывает ваш JS и разметку, но вы иногда будете видеть, что оно используется с быстрыми и грязными взломами:

<button onclick = "alert ('Hello')"> поздороваться </ button>

Более обычным примером этого в чистом JavaScript является использование функции addEventListener. В приведенном ниже примере мы добавили анонимный прослушиватель «click» к первому элементу абзаца на странице:

// Обратите внимание, что нам нужно извлечь первый // элемент из коллекции, которой нам будет предоставлен документ .getElementsByTagName ("p") [0] .addEventListener ("click", function () {console.log ("Нажал на first <p>! ");});

Анонимная функция, которую мы предоставили addEventListener, сохраняется вашим браузером и отправляется обратно в JavaScript для запуска всякий раз, когда обнаруживается событие «click» для этого конкретного элемента.

Использовать слушателей действительно так просто.

Обзор кода

Важные фрагменты кода из этого урока

// Извлечь в конец очереди setTimeout (function () {...}, 0); // Добавить прослушиватель кликов хакерским способом <button onclick = "alert ('Hello')"> Say hello </ button> // Добавить прослушиватель кликов способом JavaScript var myEl = document.getElementsByTagName ("p") [0 ]; myEl.addEventListener ("click", function () {console.log ("Clicked the first <p>!");});

Завершение

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

Какие переменные являются локальными для обратного вызова?
Сколько информации «обратный вызов» «знает» об исходной среде теперь, когда она сама по себе?