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


недавно я столкнулся с довольно неприятной ошибкой, в которой код загружал <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()?

17 724

17 ответов:

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

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

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

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

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


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

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

важное примечание: хотя он наиболее востребован и принят, принятый ответ @staticsan на самом деле НЕ ПРАВИЛЬНО! - см комментарий Дэвида Малдера для объяснения, почему.

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

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

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

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


подробности:

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

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

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

  2. выводит результаты расчета в результате див.

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

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


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

  1. заполнить статус " расчет... может занять ~3 минуты " в статус DIV

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

  3. выводит результаты расчета в результат div.

  4. заполните статус "расчет выполнен" в статус DIV

и вы с радостью даете приложение для пользователей, чтобы повторно протестировать.

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


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

браузер помещает все свои задачи" TODO " (как задачи пользовательского интерфейса, так и команды JavaScript), возникающие в результате событий, в один очередь. И, к сожалению, повторное рисование "статуса" DIV с новым "вычислением"..."value-это отдельный 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].
  • событие: выполнить 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 handler" из очереди и начинаем выполнять следующий элемент в очереди.
  • Примечание: так как мы уже закончили расчет, 3 минуты уже прошло для пользователя. событие повторного розыгрыша еще не произошло!!!
  • событие: повторно обращаем состояния дел с "Расчет стоимости". Мы делаем повторный розыгрыш и снимаем это с очереди.
  • событие: повторно нарисовать результат DIV со значением результата. Мы делаем повторный розыгрыш и снимаем это с очереди.
  • событие: re-draw Status DIV со значением "Done". Мы делаем повторный розыгрыш и снимаем это с очереди. Зоркие зрители могут даже заметить, что "Status DIV с "вычислительным" значением мигает за долю микросекунды -ПОСЛЕ РАСЧЕТА Закончил

Итак, основная проблема заключается в том, что событие повторного рисования для DIV "Status" помещается в очередь в конце, после события "execute line 2", которое занимает 3 минуты, поэтому фактическое повторное рисование не происходит до тех пор, пока расчет не будет выполнен.


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

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

  1. заполнить статус " расчет... может занять ~3 минуты " в статус DIV

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

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

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

  • очередь: [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)].
  • событие: re-draw статус 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);
});

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

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

зацени вот это: setTimeout

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

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

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

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

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

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

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

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

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

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 мс в современном браузере, вы можете использовать окно.postMessage() как описано здесь.

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

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

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

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

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

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

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

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

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

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

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();
}

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

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

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

то, что происходит в его коде, заключается в том, что он сначала прикрепляет обработчик событий к click событие .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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