Статьи

Перевод: Мациевский Николай aka sunnybear
Опубликована: 9 июня 2008

Как работают таймеры в JavaScript

Примечание: ниже перевод заметки John Resig "How JavaScript Timers Work", в которой автор jQuery ясно и подробно излагает тонкости работы различных методов отложенного исполнения функций. Мои комментарии по клиентской производительности далее курсивом.

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

  • var id = setTimeout(fn, delay); — создает единичный таймер, срабатывание которого выливается в вызов определенной функции после указанной задержки. Данный метод возвращает уникальный ID, с помощью которого можно в дальнейшем отменить таймер.
  • var id = setInterval(fn, delay); — похож на предыдущий метод setTimeout, но совершает вызовы заданной функции постоянно (каждый раз с заданной задержкой), пока не будет отменен.
  • clearInterval(id);, clearTimeout(id); — принимают в качестве параметр ID таймера (возвращаемый двумя предыдущими методами) и предотвращают дальнейшие вызовы таймера.

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

Рисунок 1. Схема работы таймеров в JavaScript

Поток асинхронных событий

На этой диаграмме дано довольно много информации к размышлению, однако, ее полное понимание позволит вам гораздо лучше представлять, как работают асинхронные вызовы в JavaScript. На диаграмме изображена всего одна ось — по вертикали отложено время (настенные часы, wall clock) в миллисекундах. Голубыми блоками отмечены происходящие вызовы JavaScript. Например, первый блок JavaScript исполняется примерно 18мс, блок с кликом мыши примерно 11мс и т.д..

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

Для начала, внутри первого блока JavaScript устанавливаются два таймера: на 10мс через setTimeout и на 10мс через setInterval. Из-за того, как и где этот таймер был выставлен (было выставлено 2 таймера, однако, тут и далее речь идет про один; почему так — объясняется чуть дальше), он в действительности срабатывает до того, как завершается первый блок кода. Заметим, однако, что таймер исполнится не сразу после окончания этого блока, а через некоторое время (это невозможно по причине наличия других потоков, состояние которых также нужно проверить). Вместо этого задержанная в выполнении функция посылается в очередь для выполнения в следующий свободный момент.

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

«Умная» очередь

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

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

Фактически, это мы и видим в том случае, когда вызывается обработчик после третьего интервала, при этом еще не закончил свой исполнение предыдущий обработчик. Что демонстрирует удивительный факт: постановка выполнения функции через интервалы не зависит от текущего исполнения чего бы то ни было (даже самой этой функции). Функция попадет в очередь совершенно независимо от других событий (кроме уже описанных), даже если для этого придется пожертвовать свободным временем между запусками функций (т.е. если функция будет исполняться 10мс, а интервал ее запуска составит те же 10мс, то браузер будет постоянно загружен запусками такой функции).

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

Есть ли разница?

Итак, давайте рассмотрим пример, чтобы глубже понять различия между setTimeout и setInterval.

  setTimeout(function(){
    /* Какой-то большой участок кода... */
    setTimeout(arguments.callee, 10);
  }, 10);

  setInterval(function(){
    /* Какой-то большой участок кода... */
  }, 10);

Эти два участка кода, на первый взгляд, выглядят идентичными, однако, это не так. Стоит заметить, что в случае выполнение кода с setTimeout пройдет не меньше 10мс с момента его последнего запуска (может быть, даже немного больше, но никак не меньше). Тогда как код с setInterval будет пытаться выполниться каждый 10мс, не взирая на то, когда, на самом деле, произошел последний его запуск.

Выводы

Мы много чего узнали из этих примеров, давайте соберем это вместе:

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

Теперь мои пять копеек. На данный момент важными мне кажутся два вывода. Первое, если вы планируете регулярно совершать какое-то действие (например, опрашивать сервер), лучше всего это будет производить через setInterval: этот метод будет выполнен, в общем случае, быстрее, чем setTimeout, и не будет накапливаться в стек. Однако, во-вторых, если речь идет о нагруженных вычислениях, когда вы экономите процессорное время (может быть, в ущерб своевременности обновления интерфейса), то ваш выбор — это setTimeout. Он гарантирует, что между запусками заявленной функции пройдет времени не меньше, чем требуется.

Заключение

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

Читать дальше

Все комментарии (habrahabr.ru)