在长时间运行的函数上刷新 DOM

IT技术 javascript internet-explorer firefox dom
2021-01-17 17:23:16

我有一个按钮,当它被点击时会运行一个长时间运行的功能。现在,在该功能运行时,我想更改按钮文本,但在某些浏览器(如 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 毫秒,以使其正常工作(更改按钮文本)。至少现在我觉得这很愚蠢。我可以理解它是否只需要 0 毫秒的延迟就可以工作,但是在较慢的计算机中会发生什么?也许firefox在settimeout中需要100ms?这听起来很愚蠢。我尝试了很多次,1 毫秒、10 毫秒、20 毫秒……不,它不会刷新它。只有 50 毫秒。

所以我遵循了这个主题中的建议:

在 javascript dom 操作后强制在 Internet Explorer 中刷新 DOM

所以我试过:

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();

无济于事

在 Chrome/Mac 上强制 DOM 重绘/刷新

特尔;博士:

寻找一种可靠的跨浏览器方法来在不使用 setTimeout 的情况下强制 DOM 刷新(由于需要不同的时间间隔,这取决于长时间运行的代码类型、浏览器、计算机速度和 setTimeout 需要 50 到 100 毫秒的首选解决方案,具体取决于情况)

jsfiddle:http : //jsfiddle.net/WsmUh/5/

6个回答

网页基于单线程控制器更新,一半的浏览器在 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 中的长计算的 JS 示例只是为了解释如果您没有选择执行 AJAX 调用,您将如何处理这个问题,99.99% 的时间不会是案件。

编辑

另请注意,您的赏金描述是XY 问题的经典案例

2019年编辑

在现代 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...

...
没有选项可以在服务器中完成工作,因为技术上无法在服务器中完成。
2021-03-16 17:23:16
这可能是明智的;如果它不能在服务器上完成,那么将它分割成一个带有超时的回调链至少会解锁浏览器。
2021-03-20 17:23:16
顺便说一句,你有没有研究过 webworkers,因为这听起来像是一种根本不应该在 javascript 或 webworker 中完成的事情,它旨在解决 JS 的这个限制。
2021-03-21 17:23:16
我可能会在小的 settimeouts 中拆分长时间运行的函数
2021-03-26 17:23:16

解决了它! 没有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');
}

演示在这里!

嗯……不幸的是,对于 OP,他想要一些没有 setTimeout() 的东西……这是唯一的解决方案,我尝试了 34 次不同的迭代,这是通过测试的一次。除非他愿意使用 setTimeout(),或者更好的是,使用 WebWorker ......这绝对是我会做的......这对他来说是一个。如果将鼠标移到按钮外是一个问题,我建议他将按钮做得特别大。:)
2021-03-16 17:23:16
这打破了标准的浏览器行为。在为同一元素触发 mousedown 和 mouseup 之后,单击事件才会触发。如果鼠标在元素之外时发生任一情况,则不会触发 click 事件。有问题的用例:1)在元素内按下鼠标按钮,将鼠标移到外面,然后释放按钮。动画无限期运行,但处理永远不会开始。2) 在元素外按下鼠标按钮,将鼠标移动到元素内,然后松开按钮。处理运行时没有明显的迹象表明它已开始。
2021-03-25 17:23:16
非常聪明的解决方案,+1,:-) 有支持它的文档吗?我的意思是, setTimeout(..., 0); 技巧应该有效(并且它被广泛记录),但不适用于 FireFox 中的 @MirrorMirror
2021-03-27 17:23:16
但在内部 jquery 使用 setTimeout -.- 你只是使用一个框架,我看不出这有什么聪明之处
2021-03-28 17:23:16
@KyleK 谢谢你的回答,我会试着看看我是否可以避免马特指出的有问题的情况。我不介意 setTimeout 解决方案本身,我对具有 setTimeout(..., 0) 或 setTimeout(...,1) 的解决方案没问题,我不想要的是 setTimeout(..., 50/100),我将不得不投入大量value来照顾所有浏览器
2021-04-02 17:23:16

截至 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) - 因为这会破坏页面

现在考虑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>

对于我的情况,这是此页面上的最佳答案。我有一个很重的 javascript 负载,它阻塞了 UI 线程几秒钟。它无法拆分,我想在发生这种情况时在页面上放置一个加载蒙版/微调器。您的解决方案是我可以确定性方式完成此操作的唯一方法,而不会引入带有猜测超时值的人为延迟。谢谢。
2021-03-22 17:23:16
我假设这是有效的,因为第一帧(我们想要显示消息的地方)在外部 rAF 运行时发生,然后资源猪在来自内部 rAF 的第二帧运行。整洁的 :)。
2021-04-05 17:23:16
@JonFagence 你的评论让我添加了一个例子 ^^
2021-04-06 17:23:16

正如深入事件和计时的“脚本需要花费太长时间和繁重的工作”部分所述(顺便说一下,这是一个有趣的阅读):

[...]将工作分成几个部分,然后依次安排。[...] 然后浏览器就有了“空闲时间”在各部分之间进行响应。它可以呈现并响应其他事件。访问者和浏览器都很高兴。

我敢肯定,在很多时候,任务无法拆分为更小的任务或片段。但我相信在很多其他时候这也是可能的!:-)

提供的示例中需要进行一些重构。您可以创建一个函数来完成您必须完成的一部分工作它可以这样开始:

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 上表现温和)。

当场,这也很容易重构到大多数长时间运行的任务。
2021-03-18 17:23:16

如果你不想使用,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>

对于内联脚本,我们用 type 标记它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事件排队(在这种情况下不会很好地工作)。

希望这可以帮助。