Почему setTimeout (fn, 0) иногда полезен?

Недавно я столкнулся с довольно неприятной ошибкой, в которой код загружал <select> динамически через JavaScript. Этот динамически загруженный <select> имел предварительно выбранное значение. В IE6 у нас уже был код для исправления выбранного <option>, потому что иногда значение <select> selectedIndex было бы не синхронизировано с выбранным атрибутом <option> index, как показано ниже:

field.selectedIndex = element.index;

Однако этот код не работал. Несмотря на то, что поле selectedIndex было установлено правильно, в результате будет выбран неправильный индекс. Однако, если я вставил инструкцию alert() в нужное время, будет выбран правильный вариант. Думая, что это может быть какая-то проблема времени, я пробовал что-то случайное, которое я видел в коде раньше:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

И это сработало!

У меня есть решение для моей проблемы, но мне неловко, что я точно не знаю, почему это устраняет мою проблему. Кто-нибудь имеет официальное объяснение? Какую проблему с браузером я избегаю, вызывая мою функцию "позже", используя setTimeout()?

621
задан 23 апр. '09 в 0:46
источник поделиться
14 ответов

Это работает, потому что вы выполняете совместную многозадачность.

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

Решение состоит в том, чтобы "приостановить" выполнение JavaScript, чтобы потоки рендеринга были догнаны. И это эффект, который делает setTimeout() с тайм-аутом 0. Это похоже на результат потока/процесса в C. Хотя кажется, что это "сразу запустить", это фактически дает браузеру возможность закончить выполнение некоторых не-JavaScript-вещей, которые ждали завершения, прежде чем приступать к этой новой части JavaScript.

(На самом деле, setTimeout() переупорядочивает новый JavaScript в конце очереди выполнения. См. комментарии для ссылок на более подробное объяснение.)

IE6 просто оказывается более склонным к этой ошибке, но я видел, как это происходит в более старых версиях Mozilla и в Firefox.

607
ответ дан 23 апр. '09 в 3:14
источник

Введение:

ВАЖНОЕ ЗАМЕЧАНИЕ: в то время как он больше всего одобрен и принят, принятый ответ @staticsan на самом деле является NOT CORRECT! - см. комментарий Дэвида Малдера для объяснения причин.

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

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

ОБНОВЛЕНИЕ: Я сделал JSFiddle для показа - объясните ниже: http://jsfiddle.net/C2YBE/31/. Многие благодаря спасибо @ThangChung за помощь в его запуске.

UPDATE2: На всякий случай, когда веб-сайт JSFiddle умирает или удаляет код, я добавил код в этот ответ в самом конце.


ПОДРОБНОСТИ

Представьте веб-приложение с кнопкой "сделать что-нибудь" и результатом div.

Обработчик onClick для кнопки "сделать что-то" вызывает функцию "LongCalc()", которая выполняет 2 вещи:

  • Делает очень длинный расчет (например, занимает 3 минуты)

  • Распечатывает результаты вычисления в результате div.

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

Проблема очевидна - вам нужен "статус" DIV, который показывает, что происходит. Посмотрим, как это работает.


Итак, вы добавляете DIV состояния (изначально пустым) и изменяете обработчик onClick (function LongCalc()), чтобы выполнить 4 вещи:

  • Задать статус "Вычисление... может занять 3 минуты" в статусе DIV

  • Делает очень длинный расчет (например, занимает 3 минуты)

  • Распечатывает результаты вычисления в результате div.

  • Залить статус "Расчет выполнен" в состояние DIV

И вы с радостью отнесите приложение к повторному тестированию.

Они возвращаются к вам очень злобно. И объясните, что когда они нажали кнопку, статус DIV никогда не обновлялся с статусом "Вычисление..."!!!


Вы почесываете голову, задаете вопрос о StackOverflow (или читаете документы или Google) и понимаете проблему:

Браузер размещает все свои задачи "TODO" (как задачи пользовательского интерфейса, так и команды JavaScript), возникающие в результате событий, в одиночную очередь. И, к сожалению, повторное рисование "статуса" DIV с новым значением "Расчет..." представляет собой отдельный TODO, который идет в конец очереди!

Здесь происходит разбивка событий во время теста пользователя, содержимое очереди после каждого события:

  • Очередь: [Empty]
  • Событие: нажмите кнопку. Очередь после события: [Execute OnClick handler(lines 1-4)]
  • Событие: выполните первую строку в обработчике OnClick (например, измените значение статуса DIV). Очередь после события: [Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]. Обратите внимание, что, хотя изменения DOM происходят мгновенно, для повторного рисования соответствующего элемента DOM вам нужно новое событие, вызванное изменением DOM, которое было в конце очереди.
  • ПРОБЛЕМА!!! ПРОБЛЕМА!!! Детали, объясняемые ниже.
  • Событие: выполнить вторую строку в обработчике (расчет). Очередь после: [Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value].
  • Событие: выполнить третью строку в обработчике (заполнить результат DIV). Очередь после: [Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result].
  • Событие: выполнить 4-ю строку в обработчике (введите статус DIV с "DONE" ). Очередь: [Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value].
  • Событие: выполнить подразумеваемый return из onClick обработчик sub. Мы берем "Execute OnClick handler" из очереди и начинаем выполнение следующего элемента в очереди.
  • ПРИМЕЧАНИЕ. Поскольку мы уже закончили расчет, 3 минуты уже прошли для пользователя. Событие повторного розыгрыша еще не произошло.
  • Событие: повторите рисование статуса DIV с помощью значения "Расчет". Мы делаем повторную ничью и снимаем ее с очереди.
  • Событие: повторите рисование результата DIV с результатом результата. Мы делаем повторную ничью и снимаем ее с очереди.
  • Событие: повторите рисование статуса DIV со значением "Готово". Мы делаем повторную ничью и снимаем ее с очереди. Зрители с острыми глазами могут даже заметить "Status DIV с миганием" Расчет "для доли микросекунды - ПОСЛЕ РАСЧЕТА ЗАВЕРШЕНО

Итак, основная проблема заключается в том, что событие re-draw для "Status" DIV помещается в очередь в конце, ПОСЛЕ события "выполнить строку 2", которое занимает 3 минуты, поэтому фактическая повторная ничья не " t произойдет до ПОСЛЕ расчета.


На помощь приходит setTimeout(). Как это помогает? Потому что, вызывая длинный исполняемый код через setTimeout, вы фактически создаете 2 события: setTimeout сам выполнение и (из-за таймаута 0), отдельные записи очереди для исполняемого кода.

Итак, чтобы исправить вашу проблему, вы изменяете обработчик onClick как TWO-инструкции (в новой функции или только в блоке внутри onClick):

  • Задать статус "Вычисление... может занять 3 минуты" в статусе DIV

  • Выполнить setTimeout() с 0 таймаутом и вызовом функции LongCalc().

    LongCalc() функция почти такая же, как в прошлый раз, но, очевидно, не имеет статуса "Расчет..." DIV update в качестве первого шага; и вместо этого начинает расчет сразу.

Итак, как выглядит последовательность событий и очередь?

  • Очередь: [Empty]
  • Событие: нажмите кнопку. Очередь после события: [Execute OnClick handler(status update, setTimeout() call)]
  • Событие: выполните первую строку в обработчике OnClick (например, измените значение статуса DIV). Очередь после события: [Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value].
  • Событие: выполнить вторую строку в обработчике (вызов setTimeout). Очередь после: [re-draw Status DIV with "Calculating" value]. В очереди нет ничего нового в течение еще 0 секунд.
  • Событие: Тревога от таймаута гаснет, через 0 секунд. Очередь после: [re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)].
  • Событие: перерисовать статус DIV с "Расчетным" значением. Очередь после: [execute LongCalc (lines 1-3)]. Обратите внимание, что это событие повторного розыгрыша может произойти, прежде чем сигнал тревоги погаснет, что также работает.
  • ...

Ура! Статус DIV только что обновился до "Расчет..." до начала расчета!!!



Ниже приведен пример кода из JSFiddle, иллюстрирующий эти примеры: http://jsfiddle.net/C2YBE/31/:

HTML-код:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

Код JavaScript: (Выполнено в onDomReady и может потребоваться jQuery 1.9)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
492
ответ дан 01 янв. '11 в 20:53
источник

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

74
ответ дан 07 дек. '10 в 0:38
источник

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

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

Когда обновления пользовательского интерфейса генерируются, когда основной поток занят, задачи добавляются в очередь сообщений.

setTimeout(fn, 0); добавьте этот fn в конец очереди, которая будет выполнена. Он планирует добавить задачу в очередь сообщений через заданный промежуток времени.

21
ответ дан 26 февр. '13 в 0:27
источник

setTimeout() вы покупаете некоторое время, пока не будут загружены элементы DOM, даже если установлено значение 0.

Проверьте это: setTimeout

18
ответ дан 23 апр. '09 в 0:52
источник

Здесь есть противоречивые ответы, и без доказательств нет способа узнать, кому верить. Вот доказательство того, что @DVK верен, а @SalvadorDali ошибочен. Последний утверждает:

"И вот почему: невозможно установить setTimeout со временем задержка в 0 миллисекунд. Минимальное значение определяется браузера, и это не 0 миллисекунд. Исторически браузеры устанавливают это минимум до 10 миллисекунд, но спецификации HTML5 и современные браузеры установите его на 4 миллисекунды."

Минимальный тайм-аут 4 мс не имеет отношения к тому, что происходит. Что действительно происходит, так это то, что setTimeout выталкивает функцию обратного вызова в конец очереди выполнения. Если после setTimeout (callback, 0) у вас есть код блокировки, который занимает несколько секунд, то обратный вызов не будет выполняться в течение нескольких секунд, пока код блокировки не завершится. Попробуйте этот код:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

Выход:

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
15
ответ дан 26 июля '14 в 3:30
источник

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

теперь отредактируйте, что в 2015 году я должен отметить, что там также requestAnimationFrame(), что не совсем то же самое, но достаточно близко к setTimeout(fn, 0), о котором стоит упомянуть.

12
ответ дан 01 янв. '11 в 20:38
источник

Это старые вопросы со старыми ответами. Я хотел бы добавить новый взгляд на эту проблему и ответить, почему это происходит, а не почему это полезно.

Итак, у вас есть две функции:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

а затем вызовите их в следующем порядке f1(); f2();, чтобы увидеть, что второй из них выполняется первым.

И вот почему: невозможно иметь setTimeout с задержкой в ​​0 миллисекунд. Минимальное значение определяется браузером, и оно не равно 0 миллисекундам. Исторически браузер устанавливает этот минимум в 10 миллисекунд, но спецификации HTML5 и современные браузеры устанавливают его в 4 миллисекунды,

Если уровень вложенности больше 5, а тайм-аут меньше 4, то увеличить таймаут до 4.

Также из mozilla:

Чтобы реализовать тайм-аут 0 мс в современном браузере, вы можете использовать window.postMessage(), как описано здесь.

P.S. информация берется после прочтения следующей статьи.

9
ответ дан 20 мая '14 в 0:34
источник

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

8
ответ дан 01 янв. '11 в 20:39
источник

Другое дело - нажать вызов функции в нижней части стека, предотвращая переполнение стека, если вы рекурсивно вызываете функцию. Это приводит к циклу while, но позволяет механизму JavaScript запускать другие асинхронные таймеры.

4
ответ дан 21 июня '12 в 4:14
источник

Ответы на выполнение циклов и рендеринг DOM перед выполнением некоторых других кодов верны. Нулевые вторичные тайм-ауты в JavaScript помогают сделать код псевдо-многопоточным, хотя это не так.

Я хочу добавить, что значение BEST для перекрестного браузера/кросс-платформенного нулевого времени ожидания в JavaScript на самом деле составляет около 20 миллисекунд вместо 0 (ноль), поскольку многие мобильные браузеры не могут регистрировать тайм-ауты, размер которых меньше 20 миллисекунд для ограничения часов на чипах AMD.

Кроме того, длительные процессы, которые не связаны с манипулированием DOM, теперь должны быть отправлены веб-работникам, поскольку они обеспечивают истинное многопоточное выполнение JavaScript.

2
ответ дан 14 марта '13 в 2:29
источник

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

1
ответ дан 23 апр. '09 в 0:53
источник

Некоторые другие случаи, когда setTimeout полезен:

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

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

1
ответ дан 14 дек. '12 в 16:09
источник

setTimout on 0 также очень полезен в шаблоне настройки отложенного обещания, которое вы хотите сразу вернуть:

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}
1
ответ дан 02 сент. '15 в 18:39
источник

Другие вопросы по меткам или Задайте вопрос