JavaScript 具有 for 循环的顺序性质
Sequential-ish nature of JavaScript with a for-loop
既然 JavaScript 是连续的(不包括异步能力),那么为什么它不 "seem" 像这个简化的例子那样按顺序运行:
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');
};
最后,什么会导致 disabled 属性直到 在 for 循环执行后才生效?只需注释掉 remove 属性行即可在视觉上验证它。
我应该注意,不需要延迟回调、承诺或任何异步;然而,我发现的唯一解决方法是将 for 循环和剩余行包围在一个零延迟的 setTimeout 回调中,这将它放在一个新的堆栈中......但真的吗?,setTimeout 用于基本上应该逐行工作的东西?
这里到底发生了什么,为什么 setAttribute 没有在 for 循环运行之前发生?
出于效率原因,浏览器不会立即布局并显示您对 DOM 所做的每个更改。在许多情况下,DOM 更新被收集到一个批次中,然后在稍后的某个时间(比如 JS 的当前线程结束时)一次全部更新。
这样做是因为如果 Javascript 的一部分正在对 DOM 进行多项更改,重新布局文档然后在每次更改发生时重新绘制效率非常低等到 Javascript 完成执行,然后立即重新绘制所有更改。
这是一种特定于浏览器的优化方案,因此每个浏览器都会就何时重绘给定更改做出自己的实施决定,并且有些事件可以 cause/force 重绘。据我所知,这不是 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 search for "force browser repaint" 包含很多关于该主题的文章。
如果浏览器仍然不会重新绘制,其他解决方法是先隐藏,然后显示一些元素(这会导致布局变脏)或使用 setTimeout(fn, 1);
继续setTimeout 回调中的其余代码 - 从而允许浏览器有机会 "breathe" 并进行重绘,因为它认为您当前的 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);
};
The browser doesn't render changes to the DOM until the function
returns. - @Barmar
根据@Barmar 的评论和大量关于该主题的额外阅读,我将包括一个参考我的示例的摘要:
- JavaScript是单线程的,所以一次只能发生一个进程
- 渲染(重绘和回流)是浏览器执行的 separate/visual 过程,因此它在 函数完成后 出现以避免潜在的繁重 CPU/GPU如果即时渲染会导致 performance/visual 问题的计算
另一种总结方式是引用自http://javascript.info/tutorial/events-and-timing-depth#javascript-execution-and-rendering
In most browsers, rendering and JavaScript use single event queue. It means that while JavaScript is running, no rendering occurs.
换一种方式解释,我将使用我在问题中提到的 setTimeout "hack":
- 单击 "run" 按钮将我的功能置于浏览器完成的 stack/queue 事物中
- 浏览器看到"disabled"属性,然后在stack/queue任务中添加渲染进程。
- 如果我们改为将 setTimeout 添加到函数的重部分,setTimeout(按设计)将其从当前流中拉出并将其添加到函数的末尾stack/queue。这意味着初始代码行将 运行, then 禁用属性的呈现, then long-运行ning for 循环代码;全部按照队列中的堆栈顺序排列。
有关上述内容的其他资源和解释:
既然 JavaScript 是连续的(不包括异步能力),那么为什么它不 "seem" 像这个简化的例子那样按顺序运行:
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');
};
最后,什么会导致 disabled 属性直到 在 for 循环执行后才生效?只需注释掉 remove 属性行即可在视觉上验证它。
我应该注意,不需要延迟回调、承诺或任何异步;然而,我发现的唯一解决方法是将 for 循环和剩余行包围在一个零延迟的 setTimeout 回调中,这将它放在一个新的堆栈中......但真的吗?,setTimeout 用于基本上应该逐行工作的东西?
这里到底发生了什么,为什么 setAttribute 没有在 for 循环运行之前发生?
出于效率原因,浏览器不会立即布局并显示您对 DOM 所做的每个更改。在许多情况下,DOM 更新被收集到一个批次中,然后在稍后的某个时间(比如 JS 的当前线程结束时)一次全部更新。
这样做是因为如果 Javascript 的一部分正在对 DOM 进行多项更改,重新布局文档然后在每次更改发生时重新绘制效率非常低等到 Javascript 完成执行,然后立即重新绘制所有更改。
这是一种特定于浏览器的优化方案,因此每个浏览器都会就何时重绘给定更改做出自己的实施决定,并且有些事件可以 cause/force 重绘。据我所知,这不是 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 search for "force browser repaint" 包含很多关于该主题的文章。
如果浏览器仍然不会重新绘制,其他解决方法是先隐藏,然后显示一些元素(这会导致布局变脏)或使用 setTimeout(fn, 1);
继续setTimeout 回调中的其余代码 - 从而允许浏览器有机会 "breathe" 并进行重绘,因为它认为您当前的 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);
};
The browser doesn't render changes to the DOM until the function returns. - @Barmar
根据@Barmar 的评论和大量关于该主题的额外阅读,我将包括一个参考我的示例的摘要:
- JavaScript是单线程的,所以一次只能发生一个进程
- 渲染(重绘和回流)是浏览器执行的 separate/visual 过程,因此它在 函数完成后 出现以避免潜在的繁重 CPU/GPU如果即时渲染会导致 performance/visual 问题的计算
另一种总结方式是引用自http://javascript.info/tutorial/events-and-timing-depth#javascript-execution-and-rendering
In most browsers, rendering and JavaScript use single event queue. It means that while JavaScript is running, no rendering occurs.
换一种方式解释,我将使用我在问题中提到的 setTimeout "hack":
- 单击 "run" 按钮将我的功能置于浏览器完成的 stack/queue 事物中
- 浏览器看到"disabled"属性,然后在stack/queue任务中添加渲染进程。
- 如果我们改为将 setTimeout 添加到函数的重部分,setTimeout(按设计)将其从当前流中拉出并将其添加到函数的末尾stack/queue。这意味着初始代码行将 运行, then 禁用属性的呈现, then long-运行ning for 循环代码;全部按照队列中的堆栈顺序排列。
有关上述内容的其他资源和解释: