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

872

Недавно я столкнулся с довольно неприятной ошибкой, когда код загружался <select>динамически через JavaScript. Этот динамически загруженный <select>имел предварительно выбранное значение. В IE6, мы уже имели код , чтобы установить выбранный <option>, потому что иногда <select>«s selectedIndexзначение будет синхронизирован с выбранным <option>» s 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()?

Дэн Лью
источник
2
Большинство вопросов описывают, почему это полезно. Если вам нужно знать, почему это происходит - прочитайте мой ответ: stackoverflow.com/a/23747597/1090562
Сальвадор Дали
18
Филипп Робертс объясняет это наилучшим образом здесь, в своем выступлении «Какого черта цикл событий?» youtube.com/watch?v=8aGhZQkoFbQ
vasa
Если вы спешите, это часть видео, где он начинает точно задавать вопрос: youtu.be/8aGhZQkoFbQ?t=14m54s . Тем не менее, все видео стоит посмотреть наверняка.
Уильям Хэмпшир
4
setTimeout(fn)так же, как setTimeout(fn, 0), кстати.
vsync

Ответы:

827

В вопросе существовало условие гонки между:

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

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

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

Ваш обходной путь был:

setTimeout(callback, 0)

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

Поэтому решение ОП заключалось в задержке примерно на 10 мс времени установки выбранного индекса. Это дало браузеру возможность инициализировать DOM, исправляя ошибку.

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


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

staticsan
источник
276
«Решение состоит в том, чтобы« приостановить »выполнение JavaScript, чтобы потоки рендеринга наверстали упущенное». Не совсем верно, что setTimeout делает добавление нового события в очередь событий браузера, и механизм рендеринга уже находится в этой очереди (не совсем верно, но достаточно близко), поэтому он выполняется перед событием setTimeout.
Дэвид Малдер
48
Да, это гораздо более подробный и гораздо более правильный ответ. Но мое «достаточно правильно» для людей, чтобы понять, почему трюк работает.
staticsan
2
@DavidMulder, это означает, что браузер анализирует CSS и рендеринг в потоке, отличном от потока выполнения javascript?
Джейсон
8
Нет, они в принципе анализируются в одном и том же потоке, в противном случае несколько строк манипулирования DOM будут постоянно вызывать повторные операции, что крайне негативно скажется на скорости выполнения.
Дэвид Малдер
30
Это видео является лучшим объяснением того, почему мы устанавливаем время
davibq
665

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

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

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

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

ОБНОВЛЕНИЕ 2: На случай, если веб-сайт JSFiddle умрет или удалит код, я добавил код к этому ответу в самом конце.


ДЕТАЛИ :

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

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

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

  2. Печатает результаты расчета в результате div.

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

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


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

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

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

  3. Печатает результаты расчета в результате div.

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

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

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


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

Браузер помещает все свои задачи «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 со значением результата. Мы делаем перерисовку и снимаем это с очереди.
  • Событие: перерисовать статус DIV со значением «Готово». Мы делаем перерисовку и снимаем это с очереди. Зрители с острыми глазами могут даже заметить, что «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);
});
DVK
источник
12
отличный ответ ДВК! Вот суть, иллюстрирующая ваш пример gist.github.com/kumikoda/5552511#file-timeout-html
kumikoda
4
Действительно классный ответ, ДВК. Просто чтобы было проще представить, я поместил этот код в jsfiddle jsfiddle.net/thangchung/LVAaV
thangchung
4
@ThangChung - я попытался сделать лучшую версию (2 кнопки, по одной для каждого случая) в JSFiddle. Это работает как демо на Chrome и IE, но не по FF по некоторым причинам - см. Jsfiddle.net/C2YBE/31 . Я спросил, почему FF не работает здесь: stackoverflow.com/questions/20747591/…
DVK
1
@DVK «Браузер помещает все свои задачи« TODO »(как задачи пользовательского интерфейса, так и команды JavaScript), возникающие в результате событий, в одну очередь». Сэр, не могли бы вы предоставить источник для этого? Imho не браузеры должны иметь разные UI (движок рендеринга) и потоки JS .... без обид, предназначенных ... просто хочу учиться ..
bhavya_w
1
@bhavya_w Нет, все происходит в одном потоке. Это причина, по которой длинные вычисления JS могут блокировать пользовательский интерфейс
Seba Kerckhof
88

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

Энди
источник
23

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

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

Хосе Базилио
источник
23

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

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

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

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

арли
источник
«Каждое выполнение JavaScript и задачи обновления пользовательского интерфейса добавляются в систему очереди событий браузера, а затем эти задачи отправляются в основной поток пользовательского интерфейса браузера для выполнения.» .... источник, пожалуйста?
bhavya_w
4
Высокопроизводительный JavaScript (Николас Закас, Стоян Стефанов, Росс Хармс, Жюльен Лекомт и Мэтт Суини)
Арли
Даунвот за это add this fn to the end of the queue. Наиболее важным является то, где именно setTimeoutдобавляется эта функция, конец этого цикла цикла или самое начало следующего цикла цикла.
Зеленый,
19

Здесь есть противоречивые ответы с отрицательным голосом, и без доказательств невозможно узнать, кому верить. Вот доказательство того, что @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
Владимир Корнеа
источник
Ваш ответ ничего не доказывает. Это просто показывает, что на вашей машине под в конкретной ситуации компьютер выкинет вам несколько цифр. Чтобы доказать что-то связанное, вам нужно немного больше, чем несколько строк кода и несколько цифр.
Сальвадор Дали
6
@SalvadorDali, я считаю, что мои доказательства достаточно ясны для понимания большинством людей. Я думаю, что вы чувствуете себя в обороне и не приложили усилий, чтобы понять это. Я буду рад попытаться уточнить это, но я не знаю, что вы не понимаете. Попробуйте запустить код на своем компьютере, если вы подозреваете мои результаты.
Владимир Корнеа
Я постараюсь прояснить это в последний раз. Мой ответ проясняет некоторые интересные свойства в тайм-ауте, интервале и хорошо подтверждается действительными и узнаваемыми источниками. Вы строго заявили, что это неправильно, и указали на свой фрагмент кода, который по какой-то причине вы назвали доказательством. В вашем куске кода нет ничего плохого (я не против). Я просто говорю, что мой ответ не является неправильным. Это проясняет некоторые моменты. Я собираюсь остановить эту войну, потому что не вижу смысла.
Сальвадор Дали
15

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

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

Заостренный
источник
Это как раз одно из тех мест, где я видел его использование. =)
Зайбот
FYI: этот ответ был слит здесь с stackoverflow.com/questions/4574940/...
Shog9
2
состоит в том, чтобы отложить выполнение кода до отдельного последующего цикла событий : как вы выясните последующий цикл событий ? Как вы выясните, что такое текущий цикл событий? Как вы узнаете, в каком цикле событий вы находитесь сейчас?
Зеленый
@ Зеленый хорошо, ты не очень; в действительности нет прямого представления о том, что представляет собой среда выполнения JavaScript.
Заостренный
requestAnimationFrame решил проблему, с которой я столкнулся, когда IE и Firefox не обновляли пользовательский интерфейс иногда
Дэвид
9

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

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

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 (), как описано здесь .

PS информация взята после прочтения следующей статьи .

Сальвадор Дали
источник
1
@ user2407309 Ты что, шутишь? Вы имеете в виду, что спецификация HTML5 неверна, и вы правы? Прочитайте источники, прежде чем понизить голосование и сделать серьезные заявления. Мой ответ основан на спецификации HTML и исторических данных. Вместо того, чтобы делать ответ, который объясняет совершенно одно и то же снова и снова, я добавил что-то новое, что не было показано в предыдущих ответах. Я не говорю, что это единственная причина, я просто показываю что-то новое.
Сальвадор Дали
1
Это неверно: «И вот почему: невозможно установить setTimeout с задержкой 0 миллисекунд». Это не почему. Задержка в 4 мс не имеет значения, почему setTimeout(fn,0)это полезно.
Владимир Корнеа
@ user2407309 его можно легко изменить на «добавить к причинам, указанным другими, невозможно ...». Поэтому смешно понижать голос только из-за этого, особенно если ваш собственный ответ не говорит ничего нового. Достаточно небольшого редактирования.
Сальвадор Дали
10
Сальвадор Дали: Если вы игнорируете эмоциональные аспекты войны с микропламенем здесь, вам, вероятно, придется признать, что @VladimirKornea прав. Это правда, что браузеры отображают задержку 0 мс на 4 мс, но даже если бы они этого не сделали, результаты все равно были бы такими же. Механизм управления здесь заключается в том, что код помещается в очередь, а не в стек вызовов. Взгляните на эту отличную презентацию JSConf, это может помочь прояснить проблему: youtube.com/watch?v=8aGhZQkoFbQ
hashchange
1
Меня смущает, почему вы думаете, что ваша цитата по квалифицированному минимуму 4 мс является глобальным минимумом 4 мс. Как показывает ваша цитата из спецификации HTML5, минимум составляет 4 мс, только если вы вложили вызовы на setTimeout/ setIntervalболее пяти уровней; если нет, то минимум 0 мс (что при отсутствии машин времени). Документы Mozilla расширяют это, чтобы охватить повторяющиеся, а не только вложенные случаи (таким образом, setIntervalс интервалом 0 будет перепланирование сразу несколько раз, а затем задержка дольше), но простому использованию setTimeoutс минимальным вложением разрешается немедленно ставить в очередь.
ShadowRanger
8

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

user113716
источник
FYI: этот ответ был слит здесь с stackoverflow.com/questions/4574940/...
Shog9
7

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

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

Jsfiddle обеспечивается ДВК действительно иллюстрирует проблему, но его объяснения этому не является правильным.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

DanielSmedegaardBuus
источник
3

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

Джейсон Суарес
источник
Даунвот за это push the function invocation to the bottom of the stack. То stack, о чем ты говоришь, неясно. Наиболее важным является то, где именно setTimeoutдобавляется эта функция, конец этого цикла цикла или самое начало следующего цикла цикла.
Зеленый,
1

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

Джереми
источник
1

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

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

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

fabspro
источник
2
Отключение было бы лучше сделать в событии onsubmit; это будет быстрее и гарантированно будет вызвано до технической отправки формы, так как вы можете остановить отправку.
Kris
Очень верно. Я предполагаю, что отключение onclick проще для создания прототипов, потому что вы можете просто набрать onclick = "this.disabled = true" в кнопке, тогда как отключение отправки требует немного больше работы.
Fabspro
1

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

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

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

ChrisN
источник
2
Я немного скептически отношусь к вашему ответу, но проголосовал за него, потому что он заставил меня провести дополнительное исследование стандартов браузеров. Исследуя стандарты, я иду туда, куда всегда, MDN: developer.mozilla.org/en-US/docs/Web/API/window.setTimeout HTML5 спецификация говорит 4ms. Это ничего не говорит об ограничении часов на мобильных чипах. Трудно найти источник информации, чтобы поддержать ваши заявления. Обнаружил, что Dart Language от Google вообще убрал setTimeout в пользу объекта Timer.
Джон Заброски
8
(...) потому что многие мобильные браузеры не могут регистрировать тайм-ауты менее 20 миллисекунд из-за ограничений часов (...) Каждая платформа имеет ограничения по времени из-за своих часов, и ни одна платформа не способна выполнить следующую вещь ровно через 0 мс после текущий. Тайм-аут 0 мс требует выполнения функции как можно скорее, и ограничения по времени конкретной платформы не изменяют значение этого в любом случае.
Петр Доброгост
1

setTimout в 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

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

  1. setTimeout()вызывает событие быть асинхронным, поэтому выполняется после всего синхронного кода, давая вашему элементу больше времени для загрузки. Асинхронные обратные вызовы, такие как обратный вызов setTimeout(), помещаются в очередь событий и помещаются в стек циклом событий. после того, как стек синхронного кода пуст.
  2. Значение 0 для ms в качестве второго аргумента в функции setTimeout()часто немного выше (4-10 мс в зависимости от браузера). Это немного большее время, необходимое для выполнения setTimeout()обратных вызовов, вызвано количеством 'тиков' (когда тик помещает обратный вызов в стек, если стек пуст) цикла обработки событий. Из-за производительности и срока службы батареи количество тактов в цикле событий ограничено определенной величиной менее 1000 раз в секунду.
Виллем ван дер Веен
источник
-1

Javascript является однопоточным приложением, поэтому не позволяет одновременно запускать функции, поэтому для достижения этого используются циклы событий. Так что именно то, что setTimeout (fn, 0) делает это, помещается в квест задачи, который выполняется, когда ваш стек вызовов пуст. Я знаю, что это объяснение довольно скучно, поэтому я рекомендую вам просмотреть это видео, оно поможет вам, как все работает в браузере. Проверьте это видео: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

Яни Деванг
источник