О том, как работают JavaScript таймеры

Original author: John Resig
  • Translation
Чрезвычайно важно понимать, как работают JavaScript таймеры. Зачастую их поведение не совпадает с нашим интуитивным восприятием многопоточности, и это связано с тем, что в действительности они выполняются в одном потоке. Давайте рассмотрим четыре функции, с помощью которых мы можем управлять таймерами:
  • var id = setTimeout(fn, delay); — Создает простой таймер, который вызовет заданную функцию после заданной задержки. Функция возвращает уникальный ID, с помощью которого таймер может быть приостановлен.
  • var id = setInterval(fn, delay); — Похоже на setTimeout, но непрерывно вызывает функцию с заданным интервалом (пока не будет остановлена).
  • clearInterval(id);, clearTimeout(id); — Принимает таймер ID (возвращаемый одной из функций, описанных выше) и останавливает выполнение callback'a.
Главная идея, которую нужно рассмотреть, заключается в том, что точность периода задержки таймера не гарантируется. Начнем с того, что браузер исполняет все асинхронные JavaScript-события в одном потоке (такие как клик мышью или таймеры) и только в то время, когда пришла очередь этого события. Лучше всего это демонстрирует следующая диаграмма:



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

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

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

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

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

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

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

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

Давайте рассмотрим пример, который хорошо иллюстрирует разницу между setTimeout и setInterval.
   setTimeout(function(){
     /* Some long block of code... */
     setTimeout(arguments.callee, 10);
   }, 10);
   
   setInterval(function(){
     /* Some long block of code... */
   }, 10);

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

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

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

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 20

    +3
    Четыре года назад уже был перевод этой статьи от sunnybear: «Как работают таймеры в JavaScript».

    Правда, там на Хабре выложена только первая часть перевода, а у Вас — полностью.
      0
      09.06.2008 эта статья неплохо разбежалась по интернетам. Пруф
      0
      И где картинки? Хоть бы ссылки вставили.
        0
        Спасибо.
        +3
        Можно немного дополнить Резига, указав, что при работе с интервалами при блокировке кода методами alert(), prompt(), confirm() в работе браузеров наблюдаются отличия:

        setInterval(function(){
        	alert(1);
        },2000)
        

        Получив сообщение через 2 секунды, подождите примерно еще 2 секунды, а затем закройте сообщение. Firefox 10, IE9, Opera 12 мгновенно показывают новое сообщение. Вебкитовские Chrome и Safari ожидают 2 секунды после закрытия, т.е. в них отсчёт интервала запускается только после того, как завершится текущая функция по интервалу. Это неважно в повседневном коде, но может оказаться важным при специализированно разработке (например, очереди ядра).
          0
          Это только с функциями alert/prompt/confirm? То есть в вебките эти функции приостанавливают работу локального времени внутри страницы? Или суть в поведении setInterval'а? В вебките происходит перезапуск таймера после коллбэка, а в фф/ие/о перед коллбэком?
            –1
            Запустите код — и всё поймёте сами.
          +1
          Про 10мс это, вообще то, platform depended. Собственно, в windows, setTimeout и setInterval, скорее всего, напрямую используют winApi SetTimer, по крайней мере, описание работы setInterval это дословная калька с доки по SetTimer. Отсюда и все особенности их работы.
          0
          Еще от активной вкладки зависит, это надо тоже брать в учет, вешая какие-то фоновые каллбеки в своих веб-приложениях.
            0
            Эх и почему обычные, системные, таймеры не работают также чётко, а подвержены косякам allertable состояний :(
              0
              Хороший перевод, но ничего нового. Все это есть на русском на javascript.ru
              • UFO just landed and posted this here
                  +1
                  В принципе. Поведение схоже с поведением V8, так как он в самом сердце ноды, Функция process.nextTick по сути setTimeout с минимальной задержкой, т.е. как только текущая очередь выполнения освободится наступает новый «tick», событие запускается. Поразвлекайтесь с setTimeout() с задержкой в 1 мс и process.nextTick. Разница лишь в том, что nextTick будет вызваться раньше.

                  Запустите

                  setTimeout(function(){
                    console.log('Hello timeout');
                  }, 1);
                  
                  
                  process.nextTick(function(){
                    console.log('Hello ticks');
                  })
                  


                  на выходе получите:
                  Hello ticks
                  Hello timeout
                    +1
                    Дополню для полного понимания process.nextTick добавляет вызов в самое начало очереди.
                    Проще представить очереди в виде книги: где каждая страница это блок синхронных вызовов. process.nextTick добавляет вызовы в начало следующей страницы:

                    process.nextTick(function(){
                      console.log('Page 2. Tick #1');
                    })
                    
                    process.nextTick(function(){
                      console.log('Page 2. Tick #2');
                    })
                    
                    setTimeout(function(){ console.log('Page 2. Timeout'); }, 1);
                    
                    console.log('Page 1');
                    


                    На выходе:
                    Page 1
                    Page 2. Tick #1
                    Page 2. Tick #2
                    Page 2. Timeout

                  0
                  робот переводил?
                    +1
                    Претензии?
                    0
                    А можно сделать время в setInterval меньше 1 мсек?
                    Писал дроби, но любая дробь ведет себя как единица
                      0
                      Я честно сразу вам не скажу полно, но стоит покопать в сторону Event Loop, который наверное все же не может обеспечить вам 1мс)
                      Точнее сказать, это может варьироваться от ~1мс до ~15мс

                    Only users with full accounts can post comments. Log in, please.