Синтаксическая природа 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, который я упомянул в своем вопросе:
- Нажатие на кнопку "запустить" помещает мою функцию в стек / очередь вещей для браузера, чтобы выполнить
- Увидев атрибут "disabled", браузер добавляет процесс рендеринга в стек / очередь задач.
- Если вместо этого мы добавим 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);
};