Вы думаете, setTimeout останавливает работу Javascript? Нет, таймеры в Javascript работают по-другому: они откладывают на определённое время только одну задачу.

Таймеры в Javascript (setInterval, setTimeout)

В программировании на скриптовых языках периодически возникает необходимость создать паузу – приостановить выполнение программы на некоторое время, а потом продолжить работу. Например, в сценариях VBS и PHP возможны такие методы:

VBS: wscript.sleep 1500 (остановка на 1.5 секунды)

PHP: sleep(10); (остановка на 10 секунд)

Во время подобных пауз исполняющая система (PHP или VBS) ничего не делает. Разработчик, попытавшись интуитивно использовать нечто подобное в Javascript, будет неприятно удивлён. Типичная ошибка при попытке создать паузу в Javascript выглядит так:

Вы думаете, что, когда при прохождении цикла очередь дойдёт до рисования очередной цифры, ваш setTimeout честно остановит работу Javascript, подождёт 0.9 сек., добавит в конец поля ввода нужную цифру и потом продолжит работу. Но на самом деле это не так: setInterval и setTimeout в Javascript откладывают выполнение только того действия (или функции), которое указано в скобках. В нашем примере произойдёт следующее:

  1. i = 1;
  2. откладываем добавление цифры "1" к полю ввода на 0.9 секунды;
  3. немедленно за постановкой этой задачи цикл идёт дальше: i=2;
  4. откладываем добавление цифры "2" к полю ввода на 0.9 секунды;
  5. ...

Немедленно означает, например, 1 мс (то есть несоизмеримо мало, по сравнению с 900 мс): цикл прозведёт свою работу практически мгновенно, создав несколько отложенных задач от одной и той же точки времени. Это значит, все отложенные задачи по "рисованию" будут выполнены практически в одно и то же время, без пауз между добавлением новых цифр. Цикл запускается; всё замирает на 0.9 с; и ширрр – все цифры выстреливаются в ряд одна за другой.

А как в подобном случае правильно применить setTimeout? Это сложно. Придётся вызывать функцию рекурсивно (изнутри функции ту же самую функцию), а чтобы этот процесс не был бесконечным, задать условие остановки (например, величину печатаемого числа):

И ещё переменную i придётся инициализировать вне функции – например, так:

Вот теперь всё работает, как надо (мы уменьшили время задержки с 0.9 с до 0.4 с). Но для подобных задач логичнее всё-таки применять не setTimeout а setInterval (хотя при этом понадобится две функции):

Особенность метода Javascirpt setInterval в том, что он не проходит «сам собой», его надо останавливать специальным методом clearInterval. А чтобы было понятно, что именно останавливать, задаче по отложенному действию присваивается специальнй идентификатор – таймер: window.timer1 = window.setInterval(...).

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

Вот пример страницы с несколькими Javascript-таймерами, работающими одновременно: setinterval.htm (Javascript-функции в файле setinterval.js). Работу всех таймеров страницы (кроме меню) можно остановить клавишей Esc. Все таймеры примеров опираются на «естественный» (а не абстрактное i++) отсчёт – времени или расстояния. Все «часы» специально рассинхронизированы (для наглядности). Таймеры, зависящие от расстояния, используются в «индикаторе» и в выпадающем («выезжающем») меню.

Выпадающее меню

Наше выезжающее меню – реально выезжающее (из-под «шапки»): между элементами специально оставлены зазоры, чтобы видеть, как оно выезжает. Неожиданно оказалось, что мы не можем сделать одинаково плавный выезд для списков разной длины – вероятно, из-за низкой производительности компьютера (AMD Athlon 999 МГц).

Достаточно очевидно, что для красоты и гармонии нужно, чтобы списки разных пунктов меню выпадали за одно и то же время. То есть более длинные списки должны выпадать с более высокой скоростью, более короткие – с меньшей скоростью. Казалось бы, это можно реализовать так:

  1. Устанавливаем общее время «выезжания», например, в 200 мс.
  2. Если выпадающий список имеет высоту 20 px, очевидно, что мы можем двигать его вниз по одному пикселу за интервал 10 мс – и тогда за 200 мс список вылезет весь.
  3. Если выпадающий список имеет высоту 40 px, чтобы уложиться в то же время, мы должны двигать его вниз по одному пикселу за 5 мс.

По этой логике, если выпадающий список имеет высоту 200 px, мы должны двигать его вниз по одному пикселу за 1 мс. Но такая скорость на нашем компьютере не прокатывает – браузер просто не успевает отрисовывать новое положение списка за одну миллисекунду. Да. Javascript считать успевает (что там считать-то?), а браузер (Firefox) отображать не успевает. Типичная ситуация для веб.

Поэтому более-менее уровнять время выезжания меню можно только с помощью костылей, и ещё неясно, как это будет работать на более быстром компьютере. Но мы ведь должны рассчитывать на самый медленный? Алгоритм (без учёта быстродействия компьютера) получается примерно такой:

  1. Устанавливаем общее время выезжания списка: time = 224 (ms).
  2. Устанавливаем минимальное время для одного интервала в цикле: delay = 3 (ms).
  3. Устанавливаем минимальный шаг для движения списка: offset = 1 (px).
  4. Меняем всё это в зависимости от высоты списка: 1) увеличиваем время задержки (интервала) обратно пропорционально высоте и прямо пропорционально общему времени time (при высоте 224 коэффициент равен 1); 2) если высота больше 40 px, увеличиваем минимальный шаг пропорционально высоте. Константа "40" получена опытным путём для наиболее медленного компьютера. Тесты на компьютере Pentium 4 CPU 2.53GHz выявили точно такое же число – 40. Иначе таймеры идут вразнос, списки выезжают не в ногу.

Вот теперь списки более-менее выезжают. За более-менее похожее время. На странице setinterval.htm.

А вот и Брю-ус:

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

Ну, а перед запуском только вычислить все эти maxtop и offset, а также поместить список в положение mintop. Чем и занимается «предварительная» функция slide() размером в 40 строк. А всё вместе - в файле setinterval.js. Да, и эта хрень ни хрена не будет работать без подключенного файла стилей menuroll.css.

D.M., admin

Комментарии

Дилетант 21.09.2013 00:07:08

ankhzet,

нет, это не ошибка. Сообщения ведь добавляются (форма работает). И предыдущие сообщения все видно. И синтаксических ошибок HTML отладчики не показывают. Чудо! Ну, или мой собственный костыль (велосипед).

ankhzet 02.09.2013 02:38:46

"религия не позволяет мне писать ВСЁ, что начинается с $."

Ну так замените на любой подходящий, не кросс-браузерный костыль =\ xxx.style.top =, или как-то так =\

"Я, скорее, сам учусь, излагая, упорядочивая здесь свои мысли."

Прошу прощения, был неправ =)

З.Ы. У Вас походу где-то в шаблоне ошибка, html-вывод обрывается на

<h3 id="comments1" class="messages"></h3>

<script type='text/javascript'>init();</script> </td></tr></table>

Дилетант 10.08.2013 21:36:39

Проверка новой формы добавления сообщений (ещё одна)

Дилетант 10.08.2013 21:34:55

Проверка новой формы добавления сообщений

Дилетант 09.08.2013 20:59:51

ankhzet, религия не позволяет мне писать ВСЁ, что начинается с $.

Про всё остальное вы правы: и антиспам кривой, и значения не сохраняются. Всё не могу никак собраться и переделать систему комментариев примерно как на http://dbmodern.ru/

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

ankhzet 01.08.2013 22:45:30

Кривой антиспам, не сохраняются значения полей при ошибках в вводе (вернуться в истории браузера назад нельзя – поля всеравно будут пустыми), мистические скрытые теги форматирования при невозможности отредактировать свое сообщение, ошибки перенаправления... Не User-friendly фронтенд... Как при этом еще можно кого-либо учить работать с PHP и Javascript (при том что сайт на ASP)... непонятно.

ankhzet 01.08.2013 22:39:55

И там постепенно открывающееся, а не "выезжающее" меню."

А какая разница? Или пошаманить с CSS и заменить $.css({"height": }) на $.css({"top": }) религия не позволяет?

"jQuery по-прежнему плодит пр*рков, даже не пытающихся понять, как работает javascript"

А Вы понимаете? Вы понимаете, как работает DOM в опере? Или фаерфоксе? Может Хром? ИЕ? Сафари? Вы умеете работать с выделением? Можете определить регион выделения? Кроссбраузерно? Вы можете определить абсолютные координаты произвольного элемента документа? В ИЕ? Или может в Хроме? А в опере ваш код будет работать?

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

Если вы не понимаете, для чего нужен jQuery, то это ваши проблемы. А если человеку нужно сделать всплывающую менюшку, а не кучу костылей, от которых старенький ишачек отказывается переваривать полученный скрипт, то называть его пр*рком... скажем так: подобные высказывания характеризируют вас не с лучшей стороны. Вы знаете, как работает ваш компьютер? А ядро вашей ОС? Вы знаете, что такое Kernel Mode? Вы знаете, как устоет планировщик заданий в вашей ОС? А как производится рендер в вашей IDE? Вы не знаете, как работают инструменты, которыми вы пользуетесь о_О? Вы... пр***рок? Сколько же вас расплодилось =\...

Пы.Сы. Опере 11.51, невозможно оставить комментарий (выдает пустой редирект), оставляю комм через фаерфокс

Пы.Пы.Сы. антиспам-фильтр не пропускает какое-то слово, но какое- не указано... по-очереди удалять каждое слово из сообщения, чтобы угодить кривому антиспам-фильтру =\

Пы.Пы.Пы.Сы. Не сохраняется текст сообщения автор и е-мейл при наличии ошибок (не указан автор, запрещенное слово и т.д.). Набирать по новой как-то не комильфо...

Дилетант 19.07.2013 03:30:43

ankhzet, с чеголи некропостинг? тема актуальна: jQuery по-прежнему плодит придурков, даже не пытающихся понять, как работает javascript.

И там постепенно открывающееся, а не "выезжающее" меню.

ankhzet 13.07.2013 12:23:36

некропостинг наше все -_-

пример "ролл" эффекта http://ankhzet.p.ht/lazzy-roll

код комментирован и сильно разбавлен jQuery (без которого кросс-браузерное изменение CSS атрибутов эллементов страницы и пр. заняло бы еще строк 100-200 кода %))

в принципе, не вижу смысла использовать самописные функции для подобных эффектов анимации. Все это и много больше уже сделано до нас (jQuery, Prototype, MooTools etc). Причем – кроссбраузерно =\

лесник 18.04.2012 03:51:22

ankhzet, а пример работающий посмотреть можно?

лесник 13.02.2012 19:32:00

Donnie, в функции showrever есть строка: d=d.getTime() + 24*60*60*1000

Именно эта строка определяет точку, до которой идёт отсчёт (в данном случае +24 часа). Если известна абсолютная точка, из неё можно создать Дату или Время с помощью объекта Date:

var d = new Date(); d.setTime(Date.parse("2012-03-04 12:22:23"));

(см. http://javascript.ru/Date.parse)

Donnie 10.02.2012 07:32:12

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

soulman 27.01.2012 23:16:06

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

дурачек 19.01.2012 09:15:21

а если кнопку "приостановить" нажимать после достижения 100% то можно "продвигаться" дальше))))))

JS-Ламер 20.12.2011 05:53:31

всЕ, решил, спасибо большое за статью! :)

JS-Ламер 19.12.2011 12:18:36

function changeHeight(){ window.i=0; document.getElementById('Keys1').style.display="block"; window.ddd=window.setInterval(stopChange(), 400); }

function stopChange(){ document.getElementById('Keys1').style.height = i; i=i+1; if (i == 155) {clearInterval(window.ddd);}; }

почему выпадает только на 1 пиксель? я уж замучалсо

лесник 13.12.2011 18:27:03

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

Александр 08.12.2011 20:32:03

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

ankhzet 05.04.2012 10:20:51

"мы не можем сделать одинаково плавный выезд для списков разной длины" *facepalm

function roll_Init(rollElement, rollHeight, rollTime) { var now = (new Date()).getTime(); var roll = { element: rollElement, height : rollHeight, time : rollTime, start : now, end : now + rollTime, timer : null }; roll.timer = setInterval(function () {roll_Process(roll);}, 10); }

function roll_Process(roll) { var now = (new Date()).getTime(); if (now >= roll.end) roll_Stop(roll); else { var interpolation = roll.height * ((now – roll.start) / roll.time); roll.element.style.height = interpolation + 'px'; } }

function roll_Stop(roll) { clearInterval(roll.timer); roll.element.style.height = roll.height + 'px'; }

возможны синтаксические ошибки, написано на коленке. вне зависимости от производительности, в начале запуска эффекта – высота 0, через rollTime мсек – rollHeight. С каким интервалом не вызвался бы таймер, скорость увеличения высоты пропорциональна итоговой высоте. Промежуточная высота равна такой, какая была бы, если б таймер вызывался каждые rollTime / rollHeight, и высота при этом увеличивалась на 1. Для елементов любой высоты. И никаких танцев с бубном длиной в 40 строк -_-

Вадим 03.05.2011 10:16:15

спасибо большое)))

Михаил 23.04.2011 03:54:11

Очень грамотное и полезное объяснение. Спасибо

лесник 31.01.2011 20:18:30

Nataly, всегда пожалуйста. Обращайтесь с вопросами, если что!

Nataly 29.01.2011 02:33:15

Долго перебирала примеры использования setInterval, setTimeout – все примеры очень примитивны и без толкового объяснения смысла. У меня на сайте setInterval, setTimeout никак работать не хотели. И только блоагодаря этой статье все стало наконец на свои места – автору большое спасибо.