浏览器渲染和JavaScript执行的同步/异步特性

7

我有一些需要处理的内容,需要花费几秒钟的时间,因此在处理过程中,我想添加一个视觉指示器。

.processing
{
  background-color: #ff0000;
}

<div id="mydiv">
  Processing
</div>

脚本:

$("#mydiv").addClass("processing");
// Do some long running processing
$("#mydiv").removeClass("processing");

我天真地认为这个类会被应用于div,UI会被更新。然而,在浏览器中运行时(至少在Firefox中),div从未被突出显示。有人能解释一下为什么我的div从未被突出显示吗?该类被添加,处理过程进行,然后删除该类;在此过程中未更新UI,用户从未看到红色背景。
我知道JS是单线程的,但我一直认为浏览器渲染会同步进行,随着DOM的更新而同步运行。我的假设是否不正确?
更重要的是,实现这种效果的推荐方法是什么?我必须使用setTimeout使缓慢处理异步化并使用回调吗?肯定有更好的方法,因为我在这里没有任何需要异步行为的需求;我只想刷新UI。
编辑:
JSFiddle:http://jsfiddle.net/SE8wD/5/ (注意,您可能需要调整循环迭代次数以在自己的PC上获得合理的延迟)

1
我相信这样的更改将直接应用于DOM,但浏览器会在执行JavaScript之前等待“空闲”状态才进行任何重绘。 - pimvdb
我真的看不出你的代码哪里出了问题,因为你发布的代码看起来很好。也许你的外部CSS文件中有一些!important。你确定类被添加了吗(你通过firebug检查过了吗),这个过程确实需要那么长时间吗?也许它发生得太快了。 - mas-designs
@chumkiu 在他的代码中使用ajax或setttimeout是不起作用的,因为添加和删除操作不会等待异步操作完成! - mas-designs
@EvilP 没错。他的假设是正确的,所以其他地方肯定出了问题。 - Luca Rainone
Opera在执行fiddle时显示一个红色的“正在刷新”段落。 - Bergi
显示剩余3条评论
4个回答

2
你可能应该将处理过程放在单独的事件中,像这样;
$("#mydiv").addClass("processing");
setTimeout(function(){
   // This runs as a seperate event after
   // Do some long running processing
   $("#mydiv").removeClass("processing");
},1);

那样,浏览器应该会使用processing重新绘制屏幕,同时长时间运行的步骤将作为单独的事件启动,并在完成后删除处理消息。

谢谢,这正是我所怀疑的。我真的希望有一种方法可以避免强制浏览器在 DOM 更改后重新渲染。 - njr101
警告 - 我发现它在5毫秒的超时后开始工作 - 任何更短的时间,Chrome仍然会等待。 - user2486570
@user2486570 -- 我有所怀疑 -- JS 是单线程事件循环 -- 因此它只有那些由你自己创建的竞争条件 -- 如果你看到一个5毫秒的问题,可能是因为你有其他正在运行的计时器或DOM准备事件与你的其他代码冲突。 - Soren
不行 - 浏览器只有在延迟足够长的情况下才会触发重绘 - 我刚刚尝试了一下,实际上,有时候即使是5毫秒也不够。 - user2486570
我的情况是与 prompt() 相关,而不是长时间运行的操作,但结果是一样的。 - user2486570

1

我不建议为了一个大型脚本而冻结浏览器。 在这种情况下,我更喜欢修改代码以避免冻结浏览器和DOM。 如果你的代码中有循环,可以使用setInterval调用一个带有延迟的函数,并在某个地方存储一些状态变量。 例如:

for(var i=0; i<1000000; i++) {
    // do something
}

可以是:

var i = 0;
var intervalID;
var _f = function() {
    // do something
    i++;
    if(i==1000000) clearInterval(intervalID);
}
intervalID  = setInterval(_f,1);

或者类似的更优化的方案。 你的代码会稍微复杂一些,速度也会变慢……但这样可以避免浏览器冻结,并且你可以制作一个高级预加载状态栏。


谢谢,这就是我提到 setTimeout 时的意思。我知道我可以这样做,但我希望避免使用异步调用增加复杂性。这会引起一系列其他问题,其中状态在异步回调发生时是不确定的。 - njr101
我知道。你也可以看看Web Worker http://www.html5rocks.com/en/tutorials/workers/basics/ - Luca Rainone
谢谢提供链接。很有趣的内容,但不幸的是对我没有帮助,因为我需要跨浏览器支持。 - njr101
@njr 最后的建议不是解决方案,而是一个“愚蠢”的技巧,在addClass之后插入alert('请等待');,这将强制浏览器进行渲染 :) - Luca Rainone

0

然而,jQuery插件目前似乎不可用。添加了一种替代方法。如果是这样的话,您可能不需要延迟执行n.parentNode.removeChild(n); 如果是这样,请随意编辑我的答案。 - Tamara Wijsman
我在我的jsFiddle上尝试了这个,但没有成功。显然我没有正确使用它。你能把它添加到Fiddle中吗? - njr101
抱歉,Fiddle不起作用。类被应用后,脚本会因为脚本错误而中止。“完成”消息从未显示。 - njr101
感谢您的帮助。我在 OP 中提到了浏览器(它是 Firefox,最新版本)。但是在 Chrome 中对我也不起作用。在两个浏览器中,都会显示红色高亮并且脚本会中止。“完成”消息从未出现。我正在查看此 Fiddle http://jsfiddle.net/SE8wD/26/ (这个确实适用于您吗?) - njr101
感谢您的所有努力。这肯定有效,尽管它实际上只是使用hide()来提供回调,而不是setTimeout()。无论哪种方式,它都会进行异步调用,这正是我真正想要避免的。那个异步调用将不会触发,直到其余的内联代码已经被处理完毕,这就需要额外的同步。 - njr101
@njr: 我现在认为我明白原因了,只是因为你不应该在UI线程上运行处理代码。此外,浏览器可以选择何时重新绘制/回流... - Tamara Wijsman

0

浏览器在何时重新绘制和重新排版方面相当自由。不同的引擎会显示不同的行为。如需进一步阅读,请参见DOM环境下何时发生回流?。在您的示例中,浏览器看到您添加了一个类并立即删除它,因此可能不会执行任何可见操作。

为避免这种情况,您可以应用强制重绘技巧,或使用WebWorkers、setTimeout或其他方法使计算异步化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接