我有一个按钮,当它被点击时运行一个长时间运行的功能。现在,当函数运行时,我想更改按钮文本,但我在一些浏览器(如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),但文本不变。
我必须将时间设置为50ms而不是仅仅0ms,以便使其工作(更改按钮文本)。至少现在我觉得这很蠢。我能理解它是否能在0毫秒的延迟下工作,但在较慢的计算机上会发生什么?也许firefox在settimeout中需要100毫秒?这听起来相当愚蠢。我试了很多次,1毫秒,10毫秒,20毫秒……不会刷新的。只有50ms
所以我遵循了这个主题的建议:
在javascript DOM操作后强制刷新浏览器中的DOM
so I try:
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不会刷新。
有人能提供一个可靠的解决方案吗?
提前感谢!
我也尝试过
$('#parentOfElementToBeRedrawn').hide().show();
to no avail
在Chrome/Mac上强制重新绘制/刷新DOM
TL;博士:
寻找一个可靠的跨浏览器的方法有一个强制的DOM刷新没有使用setTimeout(首选的解决方案,由于需要不同的时间间隔取决于类型的长时间运行的代码,浏览器,计算机速度和setTimeout需要从50到100ms的任何地方取决于情况)
jsfiddle: http://jsfiddle.net/WsmUh/5/
网页是基于单线程控制器更新的,并且有一半的浏览器在JS执行停止之前不会更新DOM或样式,从而将计算控制权交还给浏览器。这意味着,如果你设置了一些element.style.[...] = ...
,它不会踢,直到你的代码完成运行(要么完全,或者因为浏览器看到你正在做的事情,让它拦截处理几毫秒)。
你有两个问题:1)你的按钮有一个<span>
在它。去掉它,只在按钮本身上设置。innerhtml。但这当然不是真正的问题。2)你正在运行很长时间的操作,你应该认真思考为什么,在回答了为什么之后,怎么做:
如果你正在运行一个很长的计算作业,把它分割成超时回调(或者,在2019年,await/async -参见本回答末尾的注释)。你的例子并没有显示你的"长作业"实际上是什么(旋转循环不计算),但你有几个选项,这取决于你所采用的浏览器,一个GIANT书签:不要在JavaScript中运行长作业,句号。按照规范,JavaScript是一个单线程环境,因此您想要执行的任何操作都应该能够在毫秒内完成。如果不能,你确实做错了什么。
如果你需要计算困难的事情,用AJAX操作(跨浏览器通用,通常给你a)更快的处理操作和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示例只是在这里解释如果你没有AJAX调用的选项,你可能如何处理这个问题,而99.99%的情况下都不是这样。
编辑
还要注意你的赏金描述是XY问题的经典案例
2019编辑在现代JS中,await/async的概念大大改善了超时回调,所以使用它们来代替。任何await
让浏览器知道它可以安全地运行预定的更新,所以你以"结构化的方式编写代码,如果它是同步的"方式,但是你把你的函数标记为async
,然后当你调用它们时,你await
它们的输出:
async doLongCalculation() {
let firstbit = await doFirstBit();
let secondbit = await doSecondBit(firstbit);
let result = await finishUp(secondbit);
return result;
}
async doFirstBit() {
//...
}
async doSecondBit...
...
解决了!!没有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');
}
演示!
截至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>
作为一个使用示例,考虑在无限循环中使用蒙特卡罗计算pi :
使用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>
正如事件和计时深入(顺便说一下,这是一个有趣的阅读)的"脚本占用太长时间和繁重的工作"一节所述:
[…将作业拆分为各部分,各部分依次调度。[…然后,浏览器在各个部分之间有一个"空闲时间"来响应。它可以对其他事件进行渲染和反应。访问者和浏览器都很满意。
我确信有很多时候,一个任务不能被分割成更小的任务,或片段。但我敢肯定,还会有很多其他的时候,这也是可能的!: -)
在提供的示例中需要进行一些重构。您可以创建一个函数来完成必须完成的部分工作。它可以像这样开始:
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上表现温和)。
如果你不想使用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
在这种情况下不能很好地工作)。
希望这对你有帮助。
更新:我不认为在不使用超时的情况下,您可以确保避免Firefox对DOM更新的积极避免。如果你想强制重绘/DOM更新,有一些技巧可用,比如调整元素的偏移量,或者先执行hide()再执行show(),等等,但是没有什么非常有用的,过了一段时间,当这些技巧被滥用并减慢用户体验时,浏览器就会更新以忽略这些技巧。在Chrome/Mac上强制DOM重绘/刷新
其他答案看起来都具备了所需的基本元素,但我认为值得一提的是,我的实践是将所有交互式DOM更改函数包装在一个"调度"函数中,该函数处理必要的暂停,以绕过这样一个事实:Firefox在避免DOM更新方面非常积极,以便在基准测试中取得好成绩(并在浏览互联网时响应用户)。
我查看了您的JSFiddle并定制了一个分派函数,我的许多程序都依赖于这个函数。我认为这是不言自明的,你可以把它粘贴到你现有的JS小提琴,看看它是如何工作的:
$("#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++) {
//
}
}
注意:调度函数也会阻止调用同时发生,因为如果多个点击同时发生状态更新,它会严重混淆用户。
伪造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";
}
});}
试试这个
function longRunningTask(){
// Do the task here
document.getElementById("mybutt").value = "done";
}
function longrunningfunction() {
document.getElementById("mybutt").value = "doing some work";
setTimeout(function() {
longRunningTask();
}, 1);
}
有些浏览器不能很好地处理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>
我做了一个工作的例子http://jsfiddle.net/BZWbH/2/
你有没有试过添加监听器到"onmousedown"来改变按钮文本和单击事件的长运行功能。
稍微修改了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
为我做到了。试一试。尝试调整延迟以找到最佳值。
DOM buffer也存在于android的默认浏览器中,长时间运行的javascript只刷新一次DOM缓冲区,使用setTimeout(…, 50)来解决它。
我已经为async/await调整了Estradiaz的双动画帧方法:
async function waitForDisplayUpdate() {
await waitForNextAnimationFrame();
await waitForNextAnimationFrame();
}
function waitForNextAnimationFrame() {
return new Promise((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
async function main() {
const startTime = performance.now();
for (let i = 1; i <= 5; i++) {
setStatus("Step " + i);
await waitForDisplayUpdate();
wasteCpuTime(1000);
}
const elapsedTime = Math.round(performance.now() - startTime);
setStatus(`Completed in ${elapsedTime} ms`);
}
function wasteCpuTime(ms) {
const startTime = performance.now();
while (performance.now() < startTime + ms) {
if (Math.random() == 0) {
console.log("A very rare event has happened.");
}
}
}
function setStatus(s) {
document.getElementById("status").textContent = s;
}
document.addEventListener("DOMContentLoaded", main);
Status: <span id="status">Start</span>