Входные события DOM против порядка setTimeout / setInterval - PullRequest
12 голосов
/ 18 июня 2011

У меня есть блок кода JavaScript, работающий на моей странице; давайте назовем это func1. Требуется несколько миллисекунд для запуска. Пока этот код выполняется, пользователь может щелкнуть, переместить мышь, ввести ввод с клавиатуры и т. Д. У меня есть еще один блок кода, func2, который я хочу запустить после того, как все эти события ввода в очередь были разрешены. То есть я хочу обеспечить заказ:

  1. func1
  2. Все обработчики связаны с входными событиями, которые произошли во время работы func1
  3. func2

У меня такой вопрос: Достаточно ли вызова setTimeout func2, 0 в конце func1, чтобы гарантировать этот порядок во всех современных браузерах? Что, если эта строка появилась в начале func1 - что заказ, который я должен ожидать в этом случае?

Пожалуйста, подкрепите свои ответы ссылками на соответствующие спецификации или контрольными примерами.

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

// time-consuming loop...
setTimeout func2, 0

тогда только после , когда setTimeout будет запущен, любые входные события (щелчки и т. Д.), Произошедшие в течение трудоемкого цикла, будут поставлены в очередь. (Чтобы проверить это, обратите внимание, что если вы удалите, скажем, onclick обратный вызов сразу после цикла, требующего много времени, то щелчки, которые произошли во время цикла, не вызовут этот обратный вызов.) Так что func2 ставится в очередь первым и имеет приоритет.

Установка времени ожидания 1, казалось, обошла проблему в Chrome и Safari, но в Firefox я видел, как входные события разрешались после тайм-аута, равного 80 (!). Так что чисто временный подход явно не будет делать то, что я хочу.

Также недостаточно просто обернуть один setTimeout ... 0 в другой. (Я надеялся, что первый тайм-аут сработает после того, как входные события будут поставлены в очередь, а второй сработает после того, как они разрешатся. Не повезло.) Также недостаточно было добавить третий или четвертый уровень вложенности (см. Обновление 2 ниже ).

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

Вот мой последний испытательный стенд JSFiddle: http://jsfiddle.net/EJNSu/7/

Обновление 2: Частичный обходной путь заключается во вложении func2 в два тайм-аута, удаляя все обработчики входных событий в первый тайм-аут. Однако, к сожалению, это приводит к тому, что некоторые (или даже все) входные события, произошедшие во время func1, не могут быть разрешены. (Перейдите на http://jsfiddle.net/EJNSu/10/ и попробуйте быстро щелкнуть ссылку несколько раз, чтобы наблюдать за этим поведением. Сколько кликов сообщает вам предупреждение, что вы имели?) Так что это снова удивляет меня; Я не думаю, что вызов setTimeout func2, 0, где func2 устанавливает onclick в null, может предотвратить запуск этого обратного вызова в ответ на щелчок, произошедший целую секунду назад. Я хочу убедиться, что все входные события срабатывают, но моя функция сработает после них.

Обновление 3: Я опубликовал свой ответ ниже после игры на этом испытательном стенде, который светится: http://jsfiddle.net/TrevorBurnham/uJxQB/

Наведите указатель мыши на блок (запуская 1-секундный цикл блокировки), затем щелкните несколько раз. После выполнения цикла все выполненные вами щелчки воспроизводятся: обработчик click в верхнем блоке переворачивает его под другой блок, который затем получает следующий click, и так далее. Тайм-аут, вызванный обратным вызовом mouseenter, не всегда происходит после событий щелчка, и время, необходимое для возникновения событий щелчка, сильно варьируется в разных браузерах даже на одном и том же оборудовании и ОС. (Еще одна странность этого эксперимента состояла в том, что я иногда получаю несколько событий jQuery mouseenter, даже когда постоянно перемещаю мышь в поле. Не уверен, что там происходит.)

Ответы [ 6 ]

4 голосов
/ 23 июня 2011

Я думаю, что вы ошиблись в своих экспериментах.Конечно, одна проблема заключается в том, что вы боретесь с различными реализациями цикла сообщений.Другой (кажется, тот, который вы не узнали) - это другая обработка двойного щелчка.Если дважды щелкнуть ссылку, вы не получите два события click в MSIE - это скорее одно событие click и событие dblclick (для вас похоже, что второй щелчок был "проглочен").Все остальные браузеры в этом сценарии генерируют два события click и событие dblclick.Таким образом, вам также нужно обрабатывать dblclick событий.

По мере зацикливания сообщений Firefox должен быть наиболее простым в обращении.Насколько я знаю, Firefox добавляет сообщения в очередь, даже когда работает код JavaScript.Так что простого setTimeout(..., 0) достаточно для запуска кода после обработки сообщений.Однако вы должны воздержаться от сокрытия ссылки после того, как func1() будет сделано - на данный момент щелчки еще не обработаны, и они не будут вызывать обработчики событий на скрытом элементе.Обратите внимание, что даже нулевой тайм-аут не добавляется в очередь немедленно, текущие версии Firefox имеют наименьшее возможное значение тайм-аута в течение 4 миллисекунд.

MSIE похожа, только там вам нужно обрабатывать события dblclickКак я уже говорил.Кажется, что Opera тоже так работает, но ей не нравится, если вы не вызываете event.preventDefault() (или не возвращаете false из обработчика событий, что по сути то же самое).

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

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

Вот моя адаптация вашего кода: http://jsfiddle.net/KBFqn/7/

3 голосов
/ 22 июня 2011

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

Если это так, вместо использования тайм-аутов или интервалов вы можете использовать Web Workers . Все современные браузеры, включая IE9, должны поддерживать Web Workers.

Я собрал примерную страницу (не удалось поместить ее в jsfiddle, поскольку веб-работники используют внешний файл .js, который должен быть размещен в том же источнике).

Если вы нажмете A, B, C или D, сообщение будет зарегистрировано справа. При нажатии кнопки «Пуск» веб-работник начинает обработку в течение 3 секунд. Любые клики в течение этих 3 секунд будут немедленно зарегистрированы.

Важные части кода здесь:

func1.js Код, который работает внутри Web Worker

onmessage = function (e) {
    var result,
    data = e.data, // get the data passed in when this worker was called
                   // data now contains the JS literal {theData: 'to be processed by func1'}
    startTime;
    // wait for a second
    startTime = (new Date).getTime();
    while ((new Date).getTime() - startTime < 1000) {
        continue;
    }
    result = 42;
    // return our result
    postMessage(result);
}

Код, который вызывает Web Worker:

var worker = new Worker("func1.js");
// this is the callback which will fire when "func1.js" is done executing
worker.onmessage = function(event) {
    log('Func1 finished');
    func2();
};

worker.onerror = function(error) {
    throw error;
};

// send some data to be processed
log('Firing Func1');
worker.postMessage({theData: 'to be processed by func1'});
2 голосов
/ 24 июня 2011

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

Моя ментальная модель того, как работают входные события JS, была не в порядке.Я думал, что он пошел

  1. Пользователь щелкает элемент DOM во время выполнения кода
  2. Если этот элемент имеет обработчик события click, обратный вызов ставится в очередь
  3. Когда весь блокирующий код выполнен, выполняется обратный вызов

Однако мои эксперименты и результаты, представленные Владимиром Палантом (спасибо, Владимир), показывают, что правильная модель

  1. Пользователь щелкает элемент DOM во время выполнения кода
  2. Браузер фиксирует координаты и т. Д. Нажатия
  3. Через некоторое время после выполнения всего кода блокировки браузер проверяет, какиеЭлемент DOM находится в этих координатах, затем запускает обратный вызов (если есть)

Я говорю «через некоторое время», потому что разные браузеры, кажется, ведут себя по-разному для этого - в Chrome для Mac я могуустановите setTimeout func2, 0 в конце моего кода блокировки и ожидайте, что func2 будет запускаться после обратных вызовов click (которые запускаются только через 1-3 мс после завершения кода блокировки);но в Firefox тайм-аут всегда разрешается первым, и обратные вызовы кликов обычно происходят через ~ 40 мс после завершения выполнения кода блокировки.Такое поведение явно выходит за рамки спецификаций JS или DOM.Как сказал Джон Резиг в своем классическом Как работают таймеры JavaScript :

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

(выделение мое.)

Так что это означает с практической точки зрения?Это не проблема, так как время выполнения кода блокировки приближается к 0. Это означает, что эта проблема является еще одной причиной, чтобы обратить внимание на этот старый совет: Разбейте свои операции JS на маленькие куски, чтобы избежать блокировки потока.

Веб-работники, как рекомендует Бесполезный код, даже лучше, когда вы можете их использовать, но имейте в виду, что вы поддерживаете совместимость с Internet Explorer и всеми основными мобильными браузерами .

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

1 голос
/ 22 июня 2011

Вы можете использовать dispatchEvent с произвольным именем события в конце вашей функции.Это не будет работать в IE, но все еще возможно;вместо этого просто используйте fireEvent.

Взгляните на это:

http://jsfiddle.net/minitech/NsY9V/

Нажмите «начать в долгосрочной перспективе», нажмите на текстовое поле и введитеЭто.Вуаля!

0 голосов
/ 24 июня 2011

опишите лучше ваш сценарий.

Что вам нужно сделать

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

например, позвольте мне показать, как это работает

сначала зарегистрируйте все процедуры асинхронизации или синхронизации обратный вызов второго регистра Третий регистр звонков в подпрограммы с вашими параметрами четвертый брошенный процесс

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

сначала зарегистрируйте все процедуры асинхронизации или синхронизации обратный вызов второго регистра Третий регистр вызова подпрограмм с твоим параметром - зарегистрируйте свою первую рутину --register BlockUi // может быть, не принять больше изменений в представлении --register UiWriter // UoW изменений, внесенных пользователем - зарегистрировать свой последний рутина четвертый брошенный процесс

в реальном коде, который является функцией одного манекена вызова

функция Should_Can_Serializer_calls () {
RegisterMethods (модель);
model.Queue.BeginUnitProcess (); // очистить стек выполнения, и другие model.Queue.AddEndMethod (SucessfullEnd); // обратный вызов для завершения процедуры model.AbstractCall ( "func1", 1, "образование", 15 ""); // установить процедуру, как выполнить в первую очередь model.AbstractCall ( "BlockUi"); // отслеживать изменения и действия пользователя model.AbstractCall ( "UiWork"); // отслеживать изменения и действия пользователя model.AbstractCall ( "func2", "ЗНАЧЕНИЕ"); // установить вторую подпрограмму для выполнения model.Process (); // бросить вызов }

Теперь методы должны быть асинхронными для себя, для этого вы можете использовать эту библиотеку http://devedge -temp.mozilla.org / toolbox / examples / 2003 / CCallWrapper / index_en.html

Итак, что вы хотите сделать?

0 голосов
/ 22 июня 2011

Вы можете заставить обработчики событий проверять, установлен ли флаг с помощью func1; если это так, очередь func2, если еще не поставлена ​​в очередь.

Это может быть либо элегантно, либо некрасиво, в зависимости от специализации func2. (На самом деле это, вероятно, просто некрасиво.) Если вы выберете этот подход, вам понадобится какой-то способ перехватить события или, альтернативно, ваша собственная функция bindEvent(event,handler,...), которая оборачивает обработчик и связывает обернутый обработчик.

Правильность этого подхода зависит от всех событий во время func1, находящихся в очереди одновременно. Если это не так, вы можете либо сделать func2 идемпотентным, либо (в зависимости от семантики func2) поставить на него некрасивую блокировку «невозможно вызвать снова за N миллисекунд».

...