DOM刷新长时间运行的功能
Posted
技术标签:
【中文标题】DOM刷新长时间运行的功能【英文标题】:DOM refresh on long running function 【发布时间】:2013-05-28 09:40:17 【问题描述】:我有一个按钮,当它被点击时会运行一个长时间运行的功能。现在,在函数运行时,我想更改按钮文本,但在某些浏览器(如 Firefox、IE)中遇到问题。
html:
<button id="mybutt" class="buttonEnabled" onclick="longrunningfunction();"><span id="myspan">do some work</span></button>
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?听起来很愚蠢。我试了很多次,1ms、10ms、20ms……不,它不会刷新它。只有 50 毫秒。
所以我遵循了这个主题的建议:
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:
寻找一种可靠的跨浏览器方法以在不使用 setTimeout 的情况下强制刷新 DOM(首选解决方案,因为需要不同的时间间隔,具体取决于长时间运行的代码类型、浏览器、计算机速度和 setTimeout 需要 50到 100 毫秒,视情况而定)
jsfiddle:http://jsfiddle.net/WsmUh/5/
【问题讨论】:
你能把它变成一个jsfiddle吗? @Mike'Pomax'Kamermans 是的,我刚刚用 jsfiddle 更新了我的帖子。 关于您的编辑,这是否意味着 jQuery 也可以成为解决方案的一部分? 乔希,是的,我不介意 jquery 是否是解决方案的一部分 您不希望在主浏览器线程中进行繁重的处理,因为它会冻结页面。使用 Web Workers 执行任务。 【参考方案1】:截至 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>
作为一个使用示例,考虑在无限循环中使用 Monte Carlo 计算 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>
【讨论】:
我认为这是可行的,因为第一帧(我们想要显示消息的地方)发生在外部 rAF 运行时,然后资源消耗在来自内部 rAF 的第二帧处运行。整洁:)。 @JonFagence 你的评论让我添加了一个例子 ^^ 这是此页面上针对我的情况的最佳答案。我的 javascript 负载很重,会阻塞 UI 线程几秒钟。它不能被拆分,我想在发生这种情况时在页面上放置一个加载蒙版/微调器。您的解决方案是我能够以确定性方式完成此任务的唯一方法,而不会引入带有猜测超时值的人为延迟。谢谢。【参考方案2】:网页基于单线程控制器进行更新,一半的浏览器不会更新 DOM 或样式,直到您的 JS 执行停止,将计算控制权交还给浏览器。这意味着如果您设置了一些element.style.[...] = ...
,它在您的代码完成运行之前不会启动(完全运行,或者因为浏览器看到您正在做一些让它拦截处理几毫秒的事情)。
您有两个问题:1)您的按钮中有一个<span>
。删除它,只需在按钮本身上设置 .innerHTML 即可。但这当然不是真正的问题。 2)你正在运行很长的操作,你应该认真思考为什么,在回答了为什么之后,如何:
如果您正在运行一项长时间的计算作业,请将其拆分为超时回调(或者,在 2019 年,等待/异步 - 请参阅此分析器末尾的注释)。您的示例并未显示您的“长期工作”实际上是什么(自旋循环不算在内),但您有多种选择,具体取决于您使用的浏览器,有一个 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% 的时间会不是这样的。
编辑
还请注意,您的赏金描述是 The XY problem 的经典案例
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...
...
【讨论】:
没有选项可以在服务器中完成这项工作,因为从技术上讲,它无法在服务器中完成。 我可能会在小的 settimeouts 中拆分长时间运行的函数 这可能是明智的;如果不能在服务器上完成,那么将其切割成带有超时的回调链至少会解除对浏览器的阻塞。 顺便说一句,你有没有研究过 webworkers,因为这听起来像是在 javascript 或 webworker 中根本不应该做的事情,这是为了解决 JS 的这个限制。 【参考方案3】:伪造一个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";
);
【讨论】:
【参考方案4】:DOM 缓冲区也存在于 android 的默认浏览器中, 长时间运行的 javascript 只刷新一次 DOM 缓冲区, 使用 setTimeout(..., 50) 来解决。
【讨论】:
【参考方案5】:更新:我认为从长远来看,您无法确保在不使用超时的情况下避免 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】:在 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
为我解决了问题。试试吧。
尝试调整延迟以找到最佳值。
【讨论】:
【参考方案7】:解决了!!没有 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');
DEMO HERE!
【讨论】:
非常聪明的解决方案,+1,:-) 有支持它的文档吗?我的意思是, setTimeout(..., 0);技巧应该有效(并且它已被广泛记录),但不适用于 FireFox 中的 @MirrorMirror 这破坏了标准的浏览器行为。在同一元素的 mousedown 和 mouseup 触发之后,单击事件才会触发。如果在鼠标位于元素之外时发生任何一种情况,则不会触发 click 事件。有问题的用例: 1) 在元素内按下鼠标按钮,将鼠标移到外面,然后释放按钮。动画无限期地运行,但处理永远不会开始。 2)在元素外按下鼠标按钮,将鼠标移动到内部,然后释放按钮。处理运行时没有明显迹象表明它已开始。 嗯........不幸的是,对于 OP,他想要没有 setTimeout() 的东西......这是唯一的解决方案,我尝试了 34 次不同的迭代,而这个是通过测试的那个。除非他愿意使用 setTimeout(),或者更好的是,使用 WebWorker..这绝对是我会做的......这就是他的。如果将鼠标移到按钮之外是一个问题,我建议他将按钮做得特别大。 :) @KyleK 感谢您的回答,我会尝试看看是否可以避免马特指出的问题案例。我不介意 setTimeout 解决方案本身,我对具有 setTimeout(..., 0) 或 setTimeout(...,1) 的解决方案很好,我不想要的是 setTimeout(..., 50/100)我必须付出很大的代价来照顾所有浏览器 但是 jquery 在内部使用 setTimeout 全部 -.- 你只是在使用一个框架,我看不出这有什么聪明的【参考方案8】:您是否尝试将侦听器添加到“onmousedown”以更改按钮文本和单击事件以实现长时间运行功能。
【讨论】:
【参考方案9】:正如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 上似乎表现温和)。
【讨论】:
正确,这对于大多数长时间运行的任务也很容易重构。【参考方案10】:如果您不想使用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
在这种情况下无法正常工作)。
希望这会有所帮助。
【讨论】:
【参考方案11】:有些浏览器不能很好地处理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/
【讨论】:
很遗憾,不,不起作用。您更新的 jsfiddle:jsfiddle.net/BZWbH/3【参考方案12】:试试这个
function longRunningTask()
// Do the task here
document.getElementById("mybutt").value = "done";
function longrunningfunction()
document.getElementById("mybutt").value = "doing some work";
setTimeout(function()
longRunningTask();
, 1);
【讨论】:
我认为问题在于使用innerHTML
,这就是为什么我推荐使用value
是的,完整的 html 按钮代码是:
为什么按钮里面有一个span?
看来是这样。尝试删除它,因为它没有任何用处。
当您更新 myspan
而不是 mybutton
的值时,DOM 可能看不到更新。您需要更新 mybutton
的值并摆脱跨度。以上是关于DOM刷新长时间运行的功能的主要内容,如果未能解决你的问题,请参考以下文章