用js更新dom是web系统中经常出现的场景,但是有时候可能会遇到这样的情况,在更新dom之后还执行了一段运行时间可能比较长的js代码,这时你会发现,你更新的dom不会立刻在页面显现出来,而要等所有js都执行完之后才能出现。考虑以下的代码
<!DOCTYPE html>
<html>
<head>
<meta charset="gbk" />
<title>Test Refresh</title>
<script src="js/jquery-1.7.1.js"></script>
<script type="text/javascript">
function runLongOperation(){
var d = new Date().getTime();
while (1) {
var current = new Date().getTime();
if (current - d > 3000) {
console.log("good");
break;
}
}
}
$(function(){
$("#submit1").click(function(){
var $n = $("#name");
$n.hide();
runLongOperation();
});
});
</script>
</head>
<body>
<input type="text" placeholder="please input your name" name="wd" id="name"/>
<input type="button" name="submit1" id="submit1" value="Hide"/>
</body>
</html>
点击按钮后首先执行的操作是隐藏输入框name,然后执行了一段长约3秒钟的操作,你会发现点击按钮后隐藏输入框的动作并不会立刻在浏览器上体现,而是等到runLongOperation执行完之后才发生(我试了在chrome,FF和IE上均是如此)。为什么会这样呢,在stackoverflow上找到一个大神的解释,“Mozilla (maybe IE as well) will cache/delay executing changes to the DOM which affect display, so that it can calculate all the changes at once instead of repeatedly after each and every statement.”。也就是说,浏览器会cache住影响dom展现的操作,直到所有的js都执行完,这时候它可以一次性地更新需要更新的dom,这样做可能是出于性能的考虑。
通常来说,这不会有什么问题,但有的时候也许会带来一些体验上的不同。以我最近接触的一个系统的场景来说,点击一个tab页签会加载一个页面,加载完后会执行先关的初始化js,也就是在set完这个tab容器的content后还要执行一段js。不幸的是,这段js执行的时间有点长(由于历史原因这段js必须执行),大约1~2秒的时间(IE下),所以用户会在点击tab页签后并不能马上看到页面,而是看到一个空白的区域并等待一段时间才能看到正常的画面,这就给了用户不好的体验。为了消除这个影响,我们想要强制浏览器让内容先展示出来,再执行剩下的js,使用户可较快地看到结果,虽然接下来js的执行的js也会影响用户的操作(单线程),但因为用户在点开页面后通常有段时间是不做任何操作的(通常要先看一眼什么的),这段时间可以让坑爹的js执行完。
接下来的问题是如何让浏览器强制刷新更新后的dom呢,我们找到了几种方法:
1.更新dom的js执行完后执行alert,会强制浏览器刷新dom。这个方法简单有效,但无可操作性,不可能在系统中无缘无故弹出一个alert框。
2.根据stackoverflow上某位大神的说法,“To force an update (to force an immediate, synchronous reflow or relayout), your javascript should read a property that's affected by the change, e.g. the location of someSpan and otherSpan.:”,即是如果让js去读被改变的dom节点的相关属性,则可以迫使浏览器更新dom。但遵照这个思想试验只好发现没有起效,不知是否我的操作方法不当??望有高人指点!!
3.借助setTimeout,把后面影响刷新的js放在setTimeout里面执行。按我的理解,原来的一大段js是处在一个同步执行的逻辑中的,而浏览器会延迟一个dom刷新的机制也只是作用在同步代码中,如果我们将后面的js放在setTimeout里面,那相当于后面的代码处在一个异步逻辑中,浏览器就会认为当前的同步逻辑已经完成,可以刷新更改的dom了。但是,setTimeout的时间也不能设得太小,如果设为0,你会发现仍然可能不能立刻刷新更改后的dom。我想也许这跟各个浏览器的任务调度有关,当前的同步逻辑执行完之后,浏览器刷新dom的操作可能会被另一个任务抢占了,经试验把timeout的时间设在100左右较为理想。。。顺带提一句的是,js引擎都是单线程处理任务队列的,任何异步编程都是障眼法,可参考http://www.cnblogs.com/jeffwongishandsome/archive/2011/06/13/2080145.html。
最后,我的解决方案是将之前的代码改成如下形式:
$(function(){
$("#submit1").click(function(){
var $n = $("#name");
$n.hide();
setTimeout(runLongOperation, 100);
})
})