Почему модальные окна сообщений JS приостанавливают обратный отсчет на setTimeout()?

Я столкнулся с неожиданным поведением JS setTimeout, когда открыты модальные диалоговые окна, такие как alert, и я хотел бы знать причину этого.

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

setTimeout(function(){
    //alert A
    alert("10 seconds have passed for the first setTimeout")
}, 10000);
setTimeout(function(){
    //alert B
    alert("Wait for 15 seconds and press OK");
},1000);

Я ожидаю, что оповещение A отобразится сразу после того, как вы закроете оповещение B (предполагая, что вы ждали этого 15 секунд), поскольку время ожидания оповещения A составляло всего 10 секунд, и они уже прошли. Практика, однако, показывает, что обратный отсчет до оповещения А просто приостанавливается, пока открыто оповещение Б, и показывается только через прибл. Прошло еще 9 секунд после того, как вы закрыли предупреждение B, независимо от того, как долго B было открыто.

Это не кажется логичным.

Обновление. Я определенно не единственный, кто здесь запутался, потому что такое поведение приостановки тайм-аута происходит в Chrome и Internet Explorer, но не в Firefox. Firefox ведет себя так, как я и ожидал: если вы ждете 15 секунд с предупреждением B, предупреждение A появляется сразу же, как только вы его закрываете.


person Ivan Koshelev    schedule 21.10.2014    source источник
comment
Это одна из причин, почему alert не подходят, особенно с функциями синхронизации.   -  person soktinpk    schedule 24.10.2014
comment
@soktinpk Мы используем такие вещи, как Bootbox, для оповещений в наших проектах, но бывают случаи, когда это неприменимо, например, когда вам нужно предупредить пользователя о несохраненных данных при закрытии страницы. Я не могу себе представить, чтобы люди слишком часто попадали в беду из-за такого поведения, но тем не менее интересна основная разница в поведении между тремя большими браузерами.   -  person Ivan Koshelev    schedule 24.10.2014
comment
Предупреждение пользователя о несохраненных данных является вполне допустимым использованием для alert и/или confirm. Однако, почему вы хотите немного подождать, пока не появится alert?   -  person soktinpk    schedule 24.10.2014
comment
Я не собираюсь ждать, ситуация такова: допустим, вы установили таймаут на 5 секунд, а затем пользователь пытается уйти. Сообщение всплывает, и он читает его 5 секунд, прежде чем решает остаться и закончить свою работу. В FF тайм-аут сработает сразу после закрытия, но в IE и CR он будет ждать еще 5 секунд, в результате чего общее время ожидания составит 10 секунд.   -  person Ivan Koshelev    schedule 24.10.2014


Ответы (3)


Я сомневаюсь, что есть окончательный ответ на вопрос, почему и IE, и Chrome приостанавливают ожидающие таймеры до тех пор, пока alert не будет закрыт, а Firefox - нет. Я считаю, что это просто потому, что есть определенная свобода в интерпретации спецификаций W3C для alert :

Метод alert(message) при вызове должен выполнить следующие шаги:

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

  2. Освободите мьютекс хранилища.

  3. Показать данное сообщение пользователю.

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

Шаг 4 (пауза) поясняется далее здесь:

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

  1. Если какие-либо асинхронно работающие алгоритмы ожидают стабильного состояния, запустите их синхронную секцию, а затем возобновите выполнение их асинхронного алгоритма. (См. определение цикла обработки событий выше для Детали.)

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

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

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

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

В чем я уверен, так это в том, что вы не должны использовать оповещения JavaScript ни для чего другого, кроме целей отладки. Оповещения позволяют приостановить выполнение скрипта (пока какая-то асинхронная операция, например XHR, выполняется в фоновом режиме), но они довольно недружественны для пользователя. Правильным подходом было бы использование асинхронного кода с использованием промисов и, возможно, генераторов ES6/yeild (если вам нужен линейный стиль кода).

Следующий вопрос тесно связан, и там обсуждаются некоторые альтернативы alert:

person noseratio    schedule 28.10.2014

alert блокирует пользовательский интерфейс, и, поскольку Javascript является однопоточным, он будет блокировать запуск всего, пока диалоговое окно не будет закрыто.

person dnharjani    schedule 21.10.2014
comment
Вы хотели сказать «оповещение синхронно». Я это знаю, но это не ответ на вопрос. Первоначально я ожидал, что alert A setTimeout проверит текущее время сразу после возврата функции в alert B и разблокировки потока JS, после чего он увидит, что прошло более 10000 мс, и выполнит alert A. - person Ivan Koshelev; 21.10.2014

Если вам действительно нужно, вы можете использовать свой собственный таймер.

var now = Date.now();
function alertA(){
    //alert A
    alert("10 seconds have passed for the first setTimeout")
}
setTimeout(function(){
    //alert B
    alert("Wait for 15 seconds and press OK");
    setTimeout(alertA, 10000 - (Date.now() - now)); // Subtract the time passed
},1000);

Вы можете обернуть это в служебный метод:

function alertDelay(messages, timePassed) {
  if (typeof messages !== "object" || messages.length === 0) return;
  if (timePassed == null) timePassed = 0;
  var firstMessage = messages[0];
  var now = Date.now();
  setTimeout(function() {
    alert(firstMessage.message);
    alertDelay(messages.slice(1), Date.now() - now);
  }, firstMessage.delay - timePassed);
}

Использование:

alertDelay([
  {message: 'Message 1', delay: 1000},
  {message: 'Message 2', delay: 5000},
  {message: 'Message 3', delay: 10000}
]);
person soktinpk    schedule 24.10.2014
comment
Я пока не ищу обходной путь, мне просто любопытно, почему. Этот код определенно не сильно поможет, так как setTimeouts устанавливаются в разных местах. Единственный работающий способ, который я могу придумать, - это перегрузить window.setTimeout, чтобы использовать самый быстрый setInterval внутри, поддерживать список обратных вызовов с датами выполнения и проверять их по сравнению с Date.now(). Должен работать, но я не думаю, что есть проект, который действительно нуждался бы в такой точности. В любом случае, как я уже сказал, больше интересует «почему?» затем «как исправить?». - person Ivan Koshelev; 25.10.2014
comment
@IvanKoshelew Я думаю, что ответ dnharjani хорошо объясняет, почему: alert может заблокировать поток. Это действительно зависит от реализации alert. Вы не можете полагаться на него при использовании функций синхронизации. - person soktinpk; 26.10.2014
comment
Я не думаю, что это отвечает на исходный вопрос. Я знаю, почему он предотвращает обратный вызов setTimeout, пока он открыт, - неудивительно. Чего я не понимаю, так это почему он продолжает ждать после закрытия. Почему только FireFox делает так, как я ожидал? Должен ли я представить это как ошибку другим браузерам для отслеживания ошибок? Я имею в виду, поскольку с одним и тем же кодом они ведут себя по-разному - один из них должен быть неправильным. Вопрос в том, какой именно и почему? - person Ivan Koshelev; 26.10.2014