Синтаксическая природа JavaScript с циклом for

Поскольку JavaScript является последовательным (не считая асинхронных возможностей), то почему он "не выглядит" как последовательный, как в этом упрощенном примере:

HTML:

<input type="button" value="Run" onclick="run()"/>

JS:

var btn = document.querySelector('input');

var run = function() {
    console.clear();
    console.log('Running...');
    var then = Date.now();
    btn.setAttribute('disabled', 'disabled');

    // Button doesn't actually get disabled here!!????

    var result = 0.0;
    for (var i = 0; i < 1000000; i++) {
        result = i * Math.random();
    }

    /*
    *  This intentionally long-running worthless for-loop
    *  runs for 600ms on my computer (just to exaggerate this issue),
    *  meanwhile the button is still not disabled
    *  (it actually has the active state on it still
    *  from when I originally clicked it,
    *  technically allowing the user to add other instances
    *  of this function call to the single-threaded JavaScript stack).
    */

    btn.removeAttribute('disabled');

    /*
    *  The button is enabled now,
    *  but it wasn't disabled for 600ms (99.99%+) of the time!
    */

    console.log((Date.now() - then) + ' Milliseconds');
};

Наконец, что может привести к тому, что отключенный атрибут не вступит в силу, пока не произойдет выполнение цикла for? Это можно проверить визуально, просто закомментировав строку атрибута удаления.

Я должен отметить, что нет необходимости в отложенном обратном вызове, обещании или чем-то асинхронном; однако единственное, что я нашел, заключалось в том, чтобы заключить цикл for и оставшиеся строки в обратный вызов setTimeout с нулевой задержкой, который помещает его в новый стек... но на самом деле?, setTimeout для чего-то, что должно работать по существу построчно?

Что на самом деле происходит здесь, и почему setAttribute не происходит до запуска цикла for?

2 ответа

Решение

Браузер не отображает изменения в DOM, пока функция не вернется. @Barmar

За комментарии @ Barmar и множество дополнительных материалов по теме я добавлю резюме со ссылкой на мой пример:

  • JavaScript является однопоточным, поэтому одновременно может выполняться только один процесс
  • Рендеринг (перерисовка и перекомпоновка) - это отдельный / визуальный процесс, который выполняет браузер, поэтому он идет после завершения функции, чтобы избежать потенциально тяжелых вычислений ЦП / ГП, которые могут вызвать проблемы с производительностью / визуализацией, если они будут отображаться на лету

Обобщенный другой способ - это цитата из http://javascript.info/tutorial/events-and-timing-depth

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

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

  1. Нажатие на кнопку "запустить" помещает мою функцию в стек / очередь вещей для браузера, чтобы выполнить
  2. Увидев атрибут "disabled", браузер добавляет процесс рендеринга в стек / очередь задач.
  3. Если вместо этого мы добавим setTimeout к тяжелой части функции, setTimeout (по замыслу) вытянет его из текущего потока и добавит в конец стека / очереди. Это означает, что будут выполняться начальные строки кода, затем визуализация атрибута disabled, а затем долгосрочный код цикла for; все в порядке стека, как он был в очереди.

Дополнительные ресурсы и объяснения относительно вышеизложенного:

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

Это сделано потому, что если часть Javascript вносит множественные изменения в DOM, очень неэффективно ретранслировать документ, а затем перерисовывать каждое изменение по мере его появления и гораздо эффективнее ждать, пока Javascript завершит выполнение, а затем перерисовать все изменения однажды.

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

Есть некоторые свойства DOM, которые требуют законченного макета, прежде чем свойство будет точным. Доступ к этим свойствам через Javascript (даже простое их чтение) заставит браузер делать макет любых ожидающих изменений DOM и, как правило, также вызывает перерисовку. Одним из таких свойств является .offsetHeight и есть другие (хотя все в этой категории имеют одинаковый эффект).

Например, вы можете вызвать перерисовку, изменив это:

btn.setAttribute('disabled', 'disabled');

к этому:

btn.setAttribute('disabled', 'disabled');
// read the offsetHeight to force a relayout and hopefully a repaint
var x = btn.offsetHeight;

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

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

Например, вы могли бы реализовать setTimeout Как это обойти:

var btn = document.querySelector('input');

var run = function() {
    console.clear();
    console.log('Running...');
    var then = Date.now();
    btn.setAttribute('disabled', 'disabled');

    // allow a repaint here before the long-running task
    setTimeout(function() {

        var result = 0.0;
        for (var i = 0; i < 1000000; i++) {
            result = i * Math.random();
        }

        /*
        *  This intentionally long-running worthless for-loop
        *  runs for 600ms on my computer (just to exaggerate this issue),
        *  meanwhile the button is still not disabled
        *  (it actually has the active state on it still
        *  from when I originally clicked it,
        *  technically allowing the user to add other instances
        *  of this function call to the single-threaded JavaScript stack).
        */

        btn.removeAttribute('disabled');

        /*
        *  The button is enabled now,
        *  but it wasn't disabled for 600ms (99.99%+) of the time!
        */

        console.log((Date.now() - then) + ' Milliseconds');
    }, 0);

};
Другие вопросы по тегам