Почему setTimeout (fn, 0) иногда полезен? - PullRequest
789 голосов
/ 23 апреля 2009

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

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()?

Ответы [ 17 ]

751 голосов
/ 23 апреля 2009

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

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

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

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

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


См. Доклад Филиппа Робертса «Какого черта цикл событий?» для более подробного объяснения.

614 голосов
/ 01 января 2011

Предисловие:

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

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

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

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

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


ПОДРОБНОСТИ

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

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

  1. Делает очень долгий расчет (скажем, занимает 3 минуты)

  2. Печатает результаты расчета в результирующем делении.

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

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


Таким образом, вы добавляете DIV «Status» (изначально пустой) и модифицируете обработчик onclick (функция LongCalc()) для выполнения 4 вещей:

  1. Заполните статус «Расчет ... может занять ~ 3 минуты» до состояния DIV

  2. Делает очень долгий расчет (скажем, занимает 3 минуты)

  3. Печатает результаты расчета в результирующем делении.

  4. Заполните статус «Расчет выполнен» до статуса DIV

И вы с радостью отдаете приложение пользователям для повторного тестирования.

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


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

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

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

  • Очередь: [Empty]
  • Событие: нажмите кнопку. Очередь за событием: [Execute OnClick handler(lines 1-4)]
  • Событие: выполнить первую строку в обработчике OnClick (например, изменить значение Status 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].
  • Событие: выполнить 3-ю строку в обработчике (заполнить результат 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» из очереди и начинаем выполнение следующего элемента в очереди.
  • ПРИМЕЧАНИЕ. Поскольку мы уже завершили расчет, для пользователя уже прошло 3 минуты. Событие перерисовки еще не произошло !!!
  • Событие: перерисовать статус DIV со значением «Расчет». Мы делаем перерисовку и снимаем это с очереди.
  • Событие: перерисовать Result DIV со значением результата. Мы делаем перерисовку и снимаем это с очереди.
  • Событие: перерисовать Status DIV со значением «Done». Мы делаем перерисовку и снимаем это с очереди. Зрители с острым взглядом могут даже заметить, что «Status DIV» мигает значение «Calculation» в течение доли микросекунды - ПОСЛЕ ТОГО, КАК ВЫПОЛНЕНО

Таким образом, основная проблема заключается в том, что событие перерисовки для DIV «Статус» помещается в очередь в конце, ПОСЛЕ события «Выполнение строки 2», которое занимает 3 минуты, поэтому реальное перерисовывание не происходит. Это не произойдет, пока ПОСЛЕ вычисления не будет.


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

Итак, чтобы решить вашу проблему, вы модифицируете ваш обработчик onClick, чтобы он был ДВУМЯ операторами (в новой функции или просто в блоке в onClick):

  1. Заполните статус «Расчет ... может занять ~ 3 минуты» в статус DIV

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

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

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

  • Очередь: [Empty]
  • Событие: нажмите кнопку. Очередь за событием: [Execute OnClick handler(status update, setTimeout() call)]
  • Событие: выполнить первую строку в обработчике OnClick (например, изменить значение Status 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);
});
87 голосов
/ 07 декабря 2010

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

21 голосов
/ 26 февраля 2013

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

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

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

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

20 голосов
/ 23 апреля 2009

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

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

18 голосов
/ 26 июля 2014

Здесь есть противоречивые ответы с отрицательным голосом, и без доказательств невозможно узнать, кому верить. Вот доказательство того, что @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
13 голосов
/ 01 января 2011

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

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

9 голосов
/ 20 мая 2014

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

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

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. информация берется после прочтения следующей статьи .

8 голосов
/ 01 января 2011

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

7 голосов
/ 05 июня 2018

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

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

jsfiddle , предоставленный DVK, действительно иллюстрирует проблему, но его объяснение не является правильным.

Что происходит в его коде, так это то, что он сначала присоединяет обработчик события к событию click на кнопке #do.

Затем, когда вы фактически нажимаете кнопку, создается message, ссылающийся на функцию обработчика событий, которая добавляется к message queue. Когда event loop достигает этого сообщения, оно создает frame в стеке с вызовом функции для обработчика события click в jsfiddle.

И вот тут-то и становится интересно. Мы настолько привыкли считать Javascript асинхронным, что склонны упускать из виду этот крошечный факт: Любой кадр должен быть выполнен полностью, прежде чем следующий кадр может быть выполнен . Нет параллелизма, люди.

Что это значит? Это означает, что всякий раз, когда функция вызывается из очереди сообщений, она блокирует очередь до тех пор, пока сгенерированный ею стек не будет очищен. Или, в более общих чертах, он блокируется, пока функция не вернется. И он блокирует все , включая операции рендеринга DOM, прокрутки и еще много чего. Если вам нужно подтверждение, просто попробуйте увеличить продолжительность длительной операции в скрипте (например, запустить внешний цикл еще 10 раз), и вы заметите, что во время его работы вы не можете прокручивать страницу. Если он работает достаточно долго, ваш браузер спросит вас, хотите ли вы завершить процесс, потому что он делает страницу не отвечающей. Кадр выполняется, а цикл обработки событий и очередь сообщений застревают до его завершения.

Так почему же этот побочный эффект текста не обновляется? Потому что, хотя вы изменили значение элемента в DOM - вы можете console.log() его значение сразу после его изменения и увидеть, что было изменено (что показывает, почему объяснение DVK неправильно) - браузер ожидает исчерпания стека (функция-обработчик on возвращает) и, таким образом, сообщения завершается, чтобы в конечном итоге можно было выполнить сообщение, добавленное средой выполнения как реакция на нашу операцию мутации, и чтобы отразить эту мутацию в пользовательском интерфейсе.

Это потому, что мы на самом деле ожидаем завершения кода. Мы не сказали «кто-то получит это, а затем вызовет эту функцию с результатами, спасибо, и теперь я закончил с возвращением imma, делайте что угодно сейчас», как мы обычно делаем с нашим основанным на событиях асинхронным Javascript. Мы вводим функцию обработчика события щелчка, мы обновляем элемент DOM, вызываем другую функцию, другая функция работает долгое время, а затем возвращается, затем мы обновляем тот же элемент DOM, и , затем мы возвращаем из начальная функция, эффективно очищающая стек. И затем браузер может перейти к следующему сообщению в очереди, которое вполне может быть сообщением, сгенерированным нами путем запуска некоторого внутреннего события типа «on-DOM-mutation».

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

Почему тогда работает setTimeout? Это происходит потому, что он эффективно удаляет вызов длительно выполняющейся функции из своего собственного фрейма, планируя ее последующее выполнение в контексте window, чтобы он сам мог немедленно возвратить и разрешить очередь сообщений для обработки других сообщений. И идея заключается в том, что сообщение «при обновлении» пользовательского интерфейса, которое было сгенерировано нами в Javascript при изменении текста в DOM, теперь опережает сообщение, помещенное в очередь для долго выполняющейся функции, поэтому обновление пользовательского интерфейса происходит до того, как мы заблокируем в течение длительного времени.

Обратите внимание, что a) долгосрочная функция по-прежнему блокирует все, когда она запускается, и b) вам не гарантируется, что обновление пользовательского интерфейса действительно опережает его в очереди сообщений. В моем браузере Chrome в июне 2018 года значение 0 не «решает» проблему, которую демонстрирует скрипка - 10 делает. Я на самом деле немного задушен этим, потому что мне кажется логичным, что сообщение об обновлении пользовательского интерфейса должно быть поставлено в очередь перед ним, так как его триггер выполняется перед планированием долгосрочной функции, которая будет запущена «позже». Но, возможно, в движке V8 есть некоторые оптимизации, которые могут помешать, или, возможно, мне просто не хватает понимания.

Хорошо, так в чем же проблема с использованием setTimeout, и что является лучшим решением для этого конкретного случая?

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

Коллега, в неправильном понимании цикла обработки событий, попытался «обработать» Javascript, используя некоторый код рендеринга шаблонов, использующий setTimeout 0 для его рендеринга. Его больше нет здесь, чтобы спрашивать, но я могу предположить, что, возможно, он вставил таймеры для измерения скорости рендеринга (что было бы возвращением непосредственности функций) и обнаружил, что использование этого подхода приведет к невероятно быстрым ответам от этой функции.

Первая проблема очевидна; вы не можете использовать javascript, поэтому вы ничего не выиграете, пока добавляете запутывание. Во-вторых, вы теперь эффективно отсоединили рендеринг шаблона от стека возможных прослушивателей событий, которые могут ожидать, что этот шаблон был отрендерен, хотя вполне возможно, что это не так. Реальное поведение этой функции было теперь недетерминированным, как и, по незнанию, любая функция, которая будет запускать ее или зависеть от нее. Вы можете делать обоснованные предположения, но не можете правильно кодировать его поведение.

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

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

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

...