我有一个按钮,当它被点击时会运行一个长时间运行的功能。现在,在函数运行时,我想更改按钮文本,但是我在Firefox,IE等浏览器中遇到了问题。
HTML:
<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>
的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";
}
现在这在firefox和IE中有问题,(在chrome中可以正常工作)
所以我想把它放到一个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);
}
但这对firefox也不起作用!按钮被禁用,改变颜色(由于新css的应用)但文本没有改变。
我必须将时间设置为50毫秒而不是仅仅0毫秒,以使其工作(更改按钮文本)。现在我至少发现这个愚蠢。我能理解它是否只能在0ms的延迟下工作,但在较慢的计算机中会发生什么?也许firefox在settimeout需要100ms?听起来很愚蠢。我尝试了很多次,1毫秒,10毫秒,20毫秒......不,它不会刷新它。只有50ms。
所以我遵循了这个主题的建议:
Forcing a DOM refresh in Internet explorer after javascript dom manipulation
所以我试过了:
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";
}
但它不起作用(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);
}
我甚至尝试过clientHeight而不是offsetTop但没有尝试。 DOM没有刷新。
有人可以提供一个可靠的解决方案,而不是非hacky吗?
提前感谢!
正如我在这里所建议的那样我也试过了
$('#parentOfElementToBeRedrawn').hide().show();
无济于事
Force DOM redraw/refresh on Chrome/Mac
TL; DR:
寻找可靠的跨浏览器方法来强制DOM刷新而不使用setTimeout(根据长时间运行代码的类型,浏览器,计算机速度和setTimeout所需的不同时间间隔的首选解决方案需要50视情况而定为100毫秒)
jsfiddle:http://jsfiddle.net/WsmUh/5/
答案 0 :(得分:16)
网页基于单个线程控制器进行更新,一半的浏览器在JS执行停止之前不会更新DOM或样式,从而将计算控制权交还给浏览器。这意味着如果你设置了一些element.style.[...] = ...
,它将不会启动,直到你的代码完成运行(完全或者因为浏览器看到你正在做一些让它拦截处理几毫秒的东西)。
您有两个问题:1)您的按钮中有<span>
个问题。删除它,只需在按钮上设置.innerHTML即可。但这当然不是真正的问题。 2)你正在进行非常长时间的操作,你应该非常认真地思考为什么,并在回答了原因之后,如何:
如果你正在运行一个很长的计算工作,请将其切换为超时回调(或者,在2019年,等待/异步 - 请参阅此anser结尾处的注释)。你的例子没有显示你的“长工作”实际上是什么(旋转循环不算)但你有几个选项取决于你采用的浏览器,只有一个 GIANT 书签:不要在JavaScript期间运行长期工作。 JavaScript是按规范的单线程环境,因此您要执行的任何操作都应该能够在几毫秒内完成。如果不能,那你就是在做错事。
如果你需要计算困难的东西,可以通过AJAX操作将其卸载到服务器(通过浏览器通用,通常会给你一个),以便更快地处理该操作; b)30秒的时间,你可以异步地不 - 等待返回结果)或使用webworker后台线程(非常不通用)。
如果您的计算需要很长但不荒谬,请重构您的代码,以便执行具有超时喘息空间的部件:
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);
}
长时间计算几乎总是可以重构为几个步骤,要么是因为您执行了几个步骤,要么是因为您运行了相同的计算一百万次,并且可以将其切割成批次。如果你(夸大了)这个:
var resuls = [];
for(var i=0; i<1000000; i++) {
// computation is performed here
if(...) results.push(...);
}
然后你可以通过回调
将其简化为具有超时放宽功能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 :不要运行长计算,但如果必须,请让服务器为您完成工作并只使用异步AJAX调用。服务器可以更快地完成工作,并且您的页面不会阻止。
如何在客户端处理JS中的长计算的JS示例只是解释如果你没有选择进行AJAX调用你将如何处理这个问题,99.99%的时间将会不是这样的。
修改强>
另请注意,您的赏金说明是The XY problem
的经典案例2019年编辑
在现代JS中,await / async概念在超时回调时大大改进,所以请改用它们。任何await
都可以让浏览器知道它可以安全地运行预定更新,因此您可以将代码编写为“结构化,就像它是同步的”方式,但是您将函数标记为async
,然后您{ {1}}只要你打电话就输出它们:
await
答案 1 :(得分:6)
解决了它!! 没有setTimeout()!!!
在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');
}
答案 2 :(得分:3)
正如Events and timing in-depth中的“脚本占用时间过长而繁重的工作”一节所述(顺便说一句,这是一篇有趣的读物):
[...] 将作业分成,这些部分会相互调度。 [...]然后有一个“空闲时间”供浏览器在各部分之间做出响应。它可以渲染并对其他事件做出反应。访问者和浏览器都很高兴。
我确信有很多次无法将任务拆分为较小的任务或碎片。但我相信在其他许多时候这也是可能的! : - )
在提供的示例中需要进行一些重构。你可以创建一个功能来完成你需要做的工作。它可以这样开始:
function doHeavyWork(start) {
var total = 1000000000;
var fragment = 1000000;
var end = start + fragment;
// Do heavy work
for (var i = start; i < end; i++) {
//
}
完成工作后,函数应确定是否必须完成下一个工件,或者执行是否已完成:
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);
}
}
你的主要dowork()函数是这样的:
function dowork() {
// Set "working" status
document.getElementById("btn").innerHTML = "working";
// Start heavy process
doHeavyWork(0);
}
http://jsfiddle.net/WsmUh/19/处的完整工作代码(似乎在Firefox上表现得很轻松)。
答案 3 :(得分:2)
如果您不想使用setTimeout
,那么您需要WebWorker
- 这将需要支持HTML5的浏览器。
这是一种可以使用它们的方法 -
定义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>
对于内联脚本,我们使用类型javascript/worker
标记它。
在常规Javascript文件中 -
将内联脚本转换为可传递给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);
}
设置工作人员:
var ww = new Worker(getInlineJS());
从WebWorker
:
ww.onmessage = function (e) {
var msg = e.data;
document.getElementById('status').innerHTML = msg;
if (msg === 'Done!') {
alert('Next');
}
};
我们在此演示中按一下按钮开始:
document.getElementById('start').addEventListener('click', start, false);
function start() {
ww.postMessage('start');
}
这里的工作示例:
http://jsfiddle.net/AbdiasSoftware/Ls4XJ/
正如您所看到的那样,即使我们在工作者上使用忙循环,也会更新用户界面(在此示例中有进展)。这是使用基于Atom的(慢速)计算机测试的。
如果您不想或不能使用WebWorker
,则 使用setTimeout
。
这是因为这是允许您排队事件的唯一方法(在setInterval
旁边)。正如您所注意到的那样,您将需要给它几毫秒,因为这将使UI引擎“空间突破”可以说。由于JS是单线程的,因此您无法以其他方式排队事件(requestAnimationFrame
在此上下文中无法正常工作)。
希望这有帮助。
答案 4 :(得分:2)
截至2019年,人们使用 double requesAnimationFrame
跳过了一帧,而不是使用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>
使用 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>
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>
答案 5 :(得分:1)
更新:从长远来看,我不认为你可以确保避免Firefox在不使用超时的情况下积极避免DOM更新。如果你想强制重绘/ DOM更新,有一些技巧可用,比如调整元素的偏移量,或者做hide()然后show()等等,但是没有什么非常可用的,过了一会儿那些技巧被滥用并减慢用户体验,然后浏览器会更新以忽略这些技巧。有关示例,请参阅此文章及其旁边的链接文章:Force DOM redraw/refresh on Chrome/Mac
其他答案看起来他们有所需的基本元素,但我认为值得一提的是我的做法是将所有交互式DOM更改函数包装在“调度”函数中,该函数处理获取所需的必要暂停围绕这样一个事实,即Firefox在避免DOM更新方面非常积极,以便在基准测试中得分(并且在浏览互联网时能够响应用户)。
我查看了你的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++) {
//
}
}
注意:调度功能还会阻止呼叫同时发生,因为如果同时发生多次点击的状态更新,则会严重混淆用户。
答案 6 :(得分:0)
试试这个
function longRunningTask(){
// Do the task here
document.getElementById("mybutt").value = "done";
}
function longrunningfunction() {
document.getElementById("mybutt").value = "doing some work";
setTimeout(function() {
longRunningTask();
}, 1);
}
答案 7 :(得分:0)
某些浏览器无法处理onclick
html属性。最好在js
对象上使用该事件。像这样:
<button id="mybutt" class="buttonEnabled">
<span id="myspan">do some work</span>
</button>
<script type="text/javascript">
window.onload = function(){
butt = document.getElementById("mybutt");
span = document.getElementById("myspan");
butt.onclick = function () {
span.innerHTML = "doing some work";
butt.disabled = true;
butt.className = "buttonDisabled";
//long running task here
span.innerHTML = "done";
};
};
</script>
做了一个小提琴
答案 8 :(得分:0)
您是否尝试将侦听器添加到“onmousedown”以更改按钮文本并单击事件以获取长时间运行功能。
答案 9 :(得分:0)
在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);
}
超时设置为更合理的值100ms
为我做了诀窍。试试吧。
尝试调整延迟以找到最佳值。
答案 10 :(得分:0)
DOM缓冲区也存在于android的默认浏览器中, 长时间运行的javascript只冲洗一次DOM缓冲区, 使用setTimeout(...,50)来解决它。
答案 11 :(得分:0)
伪造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";
}
});}