javascript 在长时间运行的函数上刷新 DOM
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/16876394/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
DOM refresh on long running function
提问by MirrorMirror
I have a button which runs a long running function when it's clicked. Now, while the function is running, I want to change the button text, but I'm having problems in some browsers like Firefox, IE.
我有一个按钮,当它被点击时会运行一个长时间运行的功能。现在,在该功能运行时,我想更改按钮文本,但在某些浏览器(如 Firefox、IE)中遇到问题。
html:
html:
<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>
javascript:
javascript:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
Now this has problems in firefox and IE, ( in chrome it works ok )
现在这在 Firefox 和 IE 中出现问题,(在 chrome 中它可以正常工作)
So I thought to put it into a settimeout:
所以我想把它放到一个settimeout中:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
setTimeout(function() {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}, 0);
}
but this doesn't work either for firefox! the button gets disabled, changes colour ( due to the application of the new css ) but the text does not change.
但这对 Firefox 也不起作用!按钮被禁用,改变颜色(由于新 css 的应用)但文本不会改变。
I have to set the time to 50ms instead of just 0ms, in order to make it work ( change the button text ). Now I find this stupid at least. I can understand if it would work with just a 0ms delay, but what would happen in a slower computer? maybe firefox would need 100ms there in the settimeout? it sounds rather stupid. I tried many times, 1ms, 10ms, 20ms...no it won't refresh it. only with 50ms.
我必须将时间设置为 50 毫秒而不是 0 毫秒,以使其正常工作(更改按钮文本)。至少现在我觉得这很愚蠢。我可以理解它是否只需要 0 毫秒的延迟就可以工作,但是在较慢的计算机中会发生什么?也许firefox在settimeout中需要100ms?这听起来很愚蠢。我尝试了很多次,1 毫秒、10 毫秒、20 毫秒……不,它不会刷新它。只有 50 毫秒。
So I followed the advice in this topic:
所以我遵循了这个主题中的建议:
Forcing a DOM refresh in Internet explorer after javascript dom manipulation
在 javascript dom 操作后强制在 Internet Explorer 中刷新 DOM
so I tried:
所以我试过:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
var a = document.getElementById("mybutt").offsetTop; //force refresh
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
but it doesn't work ( FIREFOX 21). Then i tried:
但它不起作用(FIREFOX 21)。然后我尝试:
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
var a = document.getElementById("mybutt").offsetTop; //force refresh
var b = document.getElementById("myspan").offsetTop; //force refresh
var c = document.getElementById("mybutt").clientHeight; //force refresh
var d = document.getElementById("myspan").clientHeight; //force refresh
setTimeout(function() {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}, 0);
}
I even tried clientHeight instead of offsetTop but nothing. the DOM does not get refreshed.
我什至尝试过 clientHeight 而不是 offsetTop 但什么也没有。DOM 不会刷新。
Can someone offer a reliable solution preferrably non-hacky ?
有人可以提供一个可靠的解决方案,最好是非 hacky 吗?
thanks in advance!
提前致谢!
as suggested here i also tried
正如这里所建议的,我也尝试过
$('#parentOfElementToBeRedrawn').hide().show();
to no avail
无济于事
Force DOM redraw/refresh on Chrome/Mac
TL;DR:
特尔;博士:
looking for a RELIABLE cross-browser method to have a forced DOM refresh WITHOUT the use of setTimeout (preferred solution due to different time intervals needed depending on the type of long running code, browser, computer speed and setTimeout requires anywhere from 50 to 100ms depending on situation)
寻找一种可靠的跨浏览器方法来在不使用 setTimeout 的情况下强制 DOM 刷新(由于需要不同的时间间隔,这取决于长时间运行的代码类型、浏览器、计算机速度和 setTimeout 需要 50 到 100 毫秒的首选解决方案,具体取决于情况)
jsfiddle: http://jsfiddle.net/WsmUh/5/
jsfiddle:http: //jsfiddle.net/WsmUh/5/
采纳答案by Kylie
SOLVED IT!!No setTimeout()!!!
解决了它!没有setTimeout()!!!
Tested in Chrome 27.0.1453, Firefox 21.0, Internet 9.0.8112
在 Chrome 27.0.1453、Firefox 21.0、Internet 9.0.8112 中测试
$("#btn").on("mousedown",function(){
$('#btn').html('working');}).on('mouseup', longFunc);
function longFunc(){
//Do your long running work here...
for (i = 1; i<1003332300; i++) {}
//And on finish....
$('#btn').html('done');
}
回答by Mike 'Pomax' Kamermans
Webpages are updated based on a single thread controller, and half the browsers don't update the DOM or styling until your JS execution halts, giving computational control back to the browser. That means if you set some element.style.[...] = ...
it won't kick in until your code finishes running (either completely, or because the browser sees you're doing something that lets it intercept processing for a few ms).
网页基于单线程控制器更新,一半的浏览器在 JS 执行停止之前不会更新 DOM 或样式,将计算控制权交还给浏览器。这意味着如果你设置了一些element.style.[...] = ...
它在你的代码完成运行之前不会启动(要么完全,要么因为浏览器看到你正在做一些让它拦截处理几毫秒的事情)。
You have two problems: 1) your button has a <span>
in it. Remove that, just set .innerHTML on the button itself. But this isn't the real problem of course. 2) you're running very long operations, and you should think very hard about why, and after answering the why, how:
您有两个问题:1)您的按钮中有一个<span>
。删除它,只需在按钮本身上设置 .innerHTML 即可。但这当然不是真正的问题。2)你运行的操作时间很长,你应该非常认真地思考为什么,在回答为什么之后,如何:
If you're running a long computational job, cut it up into timeout callbacks (or, in 2019, await/async - see note at the end of this anser). Your examples don't show what your "long job" actually is (a spin loop doesn't count) but you have several options depending on the browsers you take, with one GIANTbooknote: don't run long jobs in JavaScript, period. JavaScript is a single threaded environment by specification, so any operation you want to do should be able to complete in milliseconds. If it can't, you're literally doing something wrong.
如果您正在运行一个很长的计算作业,请将其分解为超时回调(或者,在 2019 年,await/async - 请参阅此分析末尾的注释)。您的示例并未显示您的“长期工作”实际上是什么(自旋循环不算在内),但是根据您使用的浏览器,您有多种选择,其中有一个GIANT 书注:不要在 JavaScript 中运行长时间的工作,句号. JavaScript 按照规范是单线程环境,因此您想要执行的任何操作都应该能够在几毫秒内完成。如果它不能,你实际上做错了什么。
If you need to calculate difficult things, offload it to the server with an AJAX operation (universal across browsers, often giving you a) faster processing for that operation and b) a good 30 seconds of time that you can asynchronously not-wait for the result to be returned) or use a webworker background thread (very much NOT universal).
如果您需要计算困难的事情,请使用 AJAX 操作(跨浏览器通用,通常为您提供 a)该操作的更快处理和 b)一个很好的 30 秒时间,您可以异步不等待要返回的结果)或使用 webworker 后台线程(非常不通用)。
If your calculation takes long but not absurdly so, refactor your code so that you perform parts, with timeout breathing space:
如果您的计算需要很长时间但并不荒谬,请重构您的代码,以便您执行部分操作,并有超时喘息空间:
function doLongCalculation(callbackFunction) {
var partialResult = {};
// part of the work, filling partialResult
setTimeout(function(){ doSecondBit(partialResult, callbackFunction); }, 10);
}
function doSecondBit(partialResult, callbackFunction) {
// more 'part of the work', filling partialResult
setTimeout(function(){ finishUp(partialResult, callbackFunction); }, 10);
}
function finishUp(partialResult, callbackFunction) {
var result;
// do last bits, forming final result
callbackFunction(result);
}
A long calculation can almost always be refactored into several steps, either because you're performing several steps, or because you're running the same computation a million times, and can cut it up into batches. If you have (exaggerated) this:
一个长计算几乎总是可以被重构为几个步骤,要么是因为您正在执行多个步骤,要么是因为您正在运行相同的计算一百万次,并且可以将其分成批次。如果你有(夸大的)这个:
var resuls = [];
for(var i=0; i<1000000; i++) {
// computation is performed here
if(...) results.push(...);
}
then you can trivially cut this up into a timeout-relaxed function with a callback
然后你可以简单地把它切成一个带有回调的超时松弛函数
function runBatch(start, end, terminal, results, callback) {
var i;
for(var i=start; i<end; i++) {
// computation is performed here
if(...) results.push(...); }
if(i>=terminal) {
callback(results);
} else {
var inc = end-start;
setTimeout(function() {
runBatch(start+inc, end+inc, terminal, results, callback);
},10);
}
}
function dealWithResults(results) {
...
}
function doLongComputation() {
runBatch(0,1000,1000000,[],dealWithResults);
}
TL;DR: don't run long computations, but if you have to, make the server do the work for you and just use an asynchronous AJAX call. The server can do the work faster, and your page won't block.
TL;DR:不要运行长时间的计算,但如果必须,让服务器为您完成工作,只需使用异步 AJAX 调用。服务器可以更快地完成工作,并且您的页面不会被阻塞。
The JS examples of how to deal with long computations in JS at the client are only here to explain how you might deal with this problem if you don't have the option to do AJAX calls, which 99.99% of the time will not be the case.
如何在客户端处理 JS 中的长计算的 JS 示例只是为了解释如果您没有选择执行 AJAX 调用,您将如何处理这个问题,99.99% 的时间不会是案件。
edit
编辑
also note that your bounty description is a classic case of The XY problem
另请注意,您的赏金描述是XY 问题的经典案例
2019 edit
2019年编辑
In modern JS the await/async concept vastly improves upon timeout callbacks, so use those instead. Any await
lets the browser know that it can safely run scheduled updates, so you write your code in a "structured as if it's synchronous" way, but you mark your functions as async
, and then you await
their output them whenever you call them:
在现代 JS 中,await/async 概念大大改进了超时回调,因此请改用它们。Anyawait
让浏览器知道它可以安全地运行预定更新,因此您以“结构化就像同步”的方式编写代码,但是您将函数标记为async
,然后await
在调用它们时输出它们:
async doLongCalculation() {
let firstbit = await doFirstBit();
let secondbit = await doSecondBit(firstbit);
let result = await finishUp(secondbit);
return result;
}
async doFirstBit() {
//...
}
async doSecondBit...
...
回答by German Latorre
As described in the "Script taking too long and heavy jobs" section of Events and timing in-depth(an interesting reading, by the way):
正如深入事件和计时的“脚本需要花费太长时间和繁重的工作”部分所述(顺便说一下,这是一个有趣的阅读):
[...] split the job into partswhich get scheduled after each other. [...] Then there is a “free time” for the browser to respond between parts. It is can render and react on other events. Both the visitor and the browser are happy.
[...]将工作分成几个部分,然后依次安排。[...] 然后浏览器就有了“空闲时间”在各部分之间进行响应。它可以呈现并响应其他事件。访问者和浏览器都很高兴。
I am sure that there are many times in which a task cannot be splitted into smaller tasks, or fragments. But I am sure that there will be many other times in which this is possible too! :-)
我敢肯定,在很多时候,任务无法拆分为更小的任务或片段。但我相信在很多其他时候这也是可能的!:-)
Some refactoring is needed in the example provided. You could create a function to do a piece of the workyou have to do. It could begin like this:
提供的示例中需要进行一些重构。您可以创建一个函数来完成您必须完成的一部分工作。它可以这样开始:
function doHeavyWork(start) {
var total = 1000000000;
var fragment = 1000000;
var end = start + fragment;
// Do heavy work
for (var i = start; i < end; i++) {
//
}
Once the work is finished, function should determine if next work piece must be done, or if execution has finished:
一旦工作完成,函数应该确定是否必须完成下一个工作,或者执行是否已经完成:
if (end == total) {
// If we reached the end, stop and change status
document.getElementById("btn").innerHTML = "done!";
} else {
// Otherwise, process next fragment
setTimeout(function() {
doHeavyWork(end);
}, 0);
}
}
Your main dowork() function would be like this:
您的主要 dowork() 函数将是这样的:
function dowork() {
// Set "working" status
document.getElementById("btn").innerHTML = "working";
// Start heavy process
doHeavyWork(0);
}
Full working code at http://jsfiddle.net/WsmUh/19/(seems to behave gently on Firefox).
http://jsfiddle.net/WsmUh/19/ 上的完整工作代码(似乎在 Firefox 上表现温和)。
回答by German Latorre
If you don't want to use setTimeout
then you are left with WebWorker
- this will require HTML5 enabled browsers however.
如果你不想使用,setTimeout
那么你就剩下了WebWorker
- 但是这将需要支持 HTML5 的浏览器。
This is one way you can use them -
这是您可以使用它们的一种方式-
Define your HTML and an inline script (you don't have to use inline script, you can just as well give an url to an existing separate JS file):
定义您的 HTML 和内联脚本(您不必使用内联脚本,您也可以为现有的单独 JS 文件提供一个 url):
<input id="start" type="button" value="Start" />
<div id="status">Preparing worker...</div>
<script type="javascript/worker">
postMessage('Worker is ready...');
onmessage = function(e) {
if (e.data === 'start') {
//simulate heavy work..
var max = 500000000;
for (var i = 0; i < max; i++) {
if ((i % 100000) === 0) postMessage('Progress: ' + (i / max * 100).toFixed(0) + '%');
}
postMessage('Done!');
}
};
</script>
For the inline script we mark it with type javascript/worker
.
对于内联脚本,我们用 type 标记它javascript/worker
。
In the regular Javascript file -
在常规的 Javascript 文件中 -
The function that converts the inline script to a Blob-url that can be passed to a WebWorker. Note that this might note work in IE and you will have to use a regular file:
将内联脚本转换为可以传递给 WebWorker 的 Blob-url 的函数。请注意,这可能会在 IE 中起作用,您将不得不使用常规文件:
function getInlineJS() {
var js = document.querySelector('[type="javascript/worker"]').textContent;
var blob = new Blob([js], {
"type": "text\/plain"
});
return URL.createObjectURL(blob);
}
Setup worker:
安装工人:
var ww = new Worker(getInlineJS());
Receive messages (or commands) from the WebWorker
:
从以下位置接收消息(或命令)WebWorker
:
ww.onmessage = function (e) {
var msg = e.data;
document.getElementById('status').innerHTML = msg;
if (msg === 'Done!') {
alert('Next');
}
};
We kick off with a button-click in this demo:
在此演示中,我们通过单击按钮开始:
document.getElementById('start').addEventListener('click', start, false);
function start() {
ww.postMessage('start');
}
Working example here:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/
这里的工作示例:http:
//jsfiddle.net/AbdiasSoftware/Ls4XJ/
As you can see the user-interface is updated (with progress in this example) even if we're using a busy-loop on the worker. This was tested with an Atom based (slow) computer.
如您所见,即使我们在工作程序上使用繁忙循环,用户界面也会更新(在本示例中为进度)。这是用基于 Atom 的(慢速)计算机测试的。
If you don't want or can't use WebWorker
you haveto use setTimeout
.
如果您不想或不能使用WebWorker
,则必须使用setTimeout
.
This is because this is the only way (beside from setInterval
) that allow you to queue up an event. As you noticed you will need to give it a few milliseconds as this will give the UI engine "room to breeth" so-to-speak. As JS is single-threaded you cannot queue up events other ways (requestAnimationFrame
will not work well in this context).
这是因为这是setInterval
允许您将事件排队的唯一方法(除了)。正如您所注意到的,您需要给它几毫秒的时间,因为这将为 UI 引擎提供“喘息的空间”。由于 JS 是单线程的,您不能以其他方式requestAnimationFrame
将事件排队(在这种情况下不会很好地工作)。
Hope this helps.
希望这可以帮助。
回答by Estradiaz
As of 2019 one uses doublerequesAnimationFrame
to skip a frame instead of creating a race condition using setTimeout.
截至 2019 年,使用doublerequesAnimationFrame
跳过一帧,而不是使用 setTimeout 创建竞争条件。
function doRun() {
document.getElementById('app').innerHTML = 'Processing JS...';
requestAnimationFrame(() =>
requestAnimationFrame(function(){
//blocks render
confirm('Heavy load done')
document.getElementById('app').innerHTML = 'Processing JS... done';
}))
}
doRun()
<div id="app"></div>
As an usage example think of calculating pi using Monte Carlo in an endless loop:
作为使用示例,请考虑在无限循环中使用蒙特卡罗计算 pi :
using forloop to mock while(true)- as this breaks the page
使用for循环来模拟while(true)- 因为这会破坏页面
function* piMonteCarlo(r = 5, yield_cycle = 10000){
let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
while(true){
total++;
if(total === Number.MAX_SAFE_INTEGER){
break;
}
x = Math.random() * r * 2 - r;
y = Math.random() * r * 2 - r;
(Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
if(total % yield_cycle === 0){
yield 4 * hits / total
}
}
}
let pi_gen = piMonteCarlo(5, 1000), pi = 3;
for(let i = 0; i < 1000; i++){
// mocks
// while(true){
// basically last value will be rendered only
pi = pi_gen.next().value
console.log(pi)
document.getElementById('app').innerHTML = "PI: " + pi
}
<div id="app"></div>
And now think about using requestAnimationFrame
for updates in between ;)
现在考虑requestAnimationFrame
在两者之间使用更新;)
function* piMonteCarlo(r = 5, yield_cycle = 10000){
let total = 0, hits = 0, x=0, y=0, rsqrd = Math.pow(r, 2);
while(true){
total++;
if(total === Number.MAX_SAFE_INTEGER){
break;
}
x = Math.random() * r * 2 - r;
y = Math.random() * r * 2 - r;
(Math.pow(x,2) + Math.pow(y,2) < rsqrd) && hits++;
if(total % yield_cycle === 0){
yield 4 * hits / total
}
}
}
let pi_gen = piMonteCarlo(5, 1000), pi = 3;
function rAFLoop(calculate){
return new Promise(resolve => {
requestAnimationFrame( () => {
requestAnimationFrame(() => {
typeof calculate === "function" && calculate()
resolve()
})
})
})
}
let stopped = false
async function piDOM(){
while(stopped==false){
await rAFLoop(() => {
pi = pi_gen.next().value
console.log(pi)
document.getElementById('app').innerHTML = "PI: " + pi
})
}
}
function stop(){
stopped = true;
}
function start(){
if(stopped){
stopped = false
piDOM()
}
}
piDOM()
<div id="app"></div>
<button onclick="stop()">Stop</button>
<button onclick="start()">start</button>
回答by Joseph Myers
Update: I don't think in the long term that you can be sure of avoiding Firefox's aggressive avoidance of DOM updates without using a timeout. If you want to force a redraw / DOM update, there are tricks available, like adjusting the offset of elements, or doing hide() then show(), etc., but there is nothing very pretty available, and after a while when those tricks get abused and slow down user experience, then browsers get updated to ignore those tricks. See this article and the linked articles beside it for some examples: Force DOM redraw/refresh on Chrome/Mac
更新:从长远来看,我认为您不能确保在不使用超时的情况下避免 Firefox 积极避免 DOM 更新。如果你想强制重绘/DOM更新,有一些技巧可用,比如调整元素的偏移量,或者先hide()然后show()等,但没有什么非常可用的,过了一段时间,当那些技巧被滥用并降低用户体验,然后浏览器会更新以忽略这些技巧。有关示例,请参阅本文及其旁边的链接文章:Force DOM redraw/refresh on Chrome/Mac
The other answers look like they have the basic elements needed, but I thought it would be worthwhile to mention that my practice is to wrap all interactive DOM-changing functions in a "dispatch" function which handles the necessary pauses needed to get around the fact that Firefox is extremely aggressive in avoiding DOM updates in order to score well on benchmarks (and to be responsive to users while browsing the internet).
其他答案看起来像它们具有所需的基本元素,但我认为值得一提的是,我的做法是将所有交互式 DOM 更改函数包装在“调度”函数中,该函数处理绕过事实所需的必要暂停Firefox 在避免 DOM 更新方面非常积极,以便在基准测试中获得高分(并在浏览互联网时对用户做出响应)。
I looked at your JSFiddle and customized a dispatch function the one that many of my programs rely on. I think it is self-explanatory, and you can just paste it into your existing JS Fiddle to see how it works:
我查看了您的 JSFiddle 并自定义了一个调度函数,我的许多程序都依赖该函数。我认为它是不言自明的,你可以将它粘贴到你现有的 JS Fiddle 中,看看它是如何工作的:
$("#btn").on("click", function() { dispatch(this, dowork, 'working', 'done!'); });
function dispatch(me, work, working, done) {
/* work function, working message HTML string, done message HTML string */
/* only designed for a <button></button> element */
var pause = 50, old;
if (!me || me.tagName.toLowerCase() != 'button' || me.innerHTML == working) return;
old = me.innerHTML;
me.innerHTML = working;
setTimeout(function() {
work();
me.innerHTML = done;
setTimeout(function() { me.innerHTML = old; }, 1500);
}, pause);
}
function dowork() {
for (var i = 1; i<1000000000; i++) {
//
}
}
Note: the dispatching function also blocks calls from happening at the same time, because it can seriously confuse users if status updates from multiple clicks are happening together.
注意:调度功能还会阻止调用同时发生,因为如果多次点击的状态更新同时发生,它会严重混淆用户。
回答by diyism
DOM buffer also exists in default browser on android, long running javascript only flush DOM buffer once, use setTimeout(..., 50) to solve it.
DOM 缓冲区也存在于android 的默认浏览器中,长时间运行的javascript 只刷新DOM 缓冲区一次,使用setTimeout(..., 50) 来解决它。
回答by TeamEASI.com
Fake an ajax request
伪造一个ajax请求
function longrunningfunction() {
document.getElementById("myspan").innerHTML = "doing some work";
document.getElementById("mybutt").disabled = true;
document.getElementById("mybutt").className = "buttonDisabled";
$.ajax({
url: "/",
complete: function () {
//long running task here
document.getElementById("myspan").innerHTML = "done";
}
});}
回答by user1390282
Have you tried adding listener to "onmousedown" to change the button text and click event for longrunning function.
您是否尝试将侦听器添加到“onmousedown”以更改按钮文本和单击事件以获得长时间运行的功能。
回答by ElmoVanKielmo
Slightly modified your code at jsfiddle and:
在 jsfiddle 稍微修改您的代码,并且:
$("#btn").on("click", dowork);
function dowork() {
document.getElementById("btn").innerHTML = "working";
setTimeout(function() {
for (var i = 1; i<1000000000; i++) {
//
}
document.getElementById("btn").innerHTML = "done!";
}, 100);
}
Timeout set to more reasonable value 100ms
did the trick for me. Try it.
Try adjusting the latency to find the best value.
超时设置为更合理的值100ms
对我有用。试试看。尝试调整延迟以找到最佳值。