对长时间运行的函数进行DOM刷新

DOM refresh on long running function

本文关键字:DOM 刷新 函数 长时间 运行      更新时间:2023-09-26

我有一个按钮,当它被点击时运行一个长时间运行的功能。现在,当函数运行时,我想更改按钮文本,但我在一些浏览器(如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>