jQuery/Javascript - 如何等待操作后的DOM更新再继续执行函数

36

我想要做的是在执行一个耗时3-12秒的脚本(没有AJAX),更新一个简单的div以显示“正在处理...”,然后在完成后将div更新为“完成!”。

我发现div永远不会更新为“正在处理...”。如果我在该命令之后立即设置断点,则div文本确实会得到更新,因此我知道语法是正确的。在IE9、FF6和Chrome13中都有相同的行为。

即使绕过jQuery并使用基本的原始JavaScript,我也看到了相同的问题。

你可能认为这个问题有一个简单的答案。然而,由于jQuery的.html()和.text()没有回调钩子,所以这不是一个选项。它也不是动画的,所以没有.queue可以操作。

您可以使用我准备的示例代码自行测试,该代码显示了具有5秒高CPU函数的jQuery和JavaScript实现。代码易于跟踪。当你点击按钮或链接时,你永远不会看到“正在处理...”。

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" ></script>
<script type="text/javascript">
function addSecs(d, s) {return new Date(d.valueOf()+s*1000);}
function doRun() {
    document.getElementById('msg').innerHTML = 'Processing JS...';
    start = new Date();
    end = addSecs(start,5);
    do {start = new Date();} while (end-start > 0);
    document.getElementById('msg').innerHTML = 'Finished JS';   
}
$(function() {
    $('button').click(function(){
        $('div').text('Processing JQ...');  
        start = new Date();
        end = addSecs(start,5);
        do {start = new Date();} while (end-start > 0);
        $('div').text('Finished JQ');   
    });
});
</script>
</head>
<body>
    <div id="msg">Not Started</div>
    <button>jQuery</button>
    <a href="#" onclick="doRun()">javascript</a>
</body>
</html>

能否查询准备标志以检查页面是否已完成加载?如果DOM正在重新计算,则它处于与初始布局页面相同的状态,并应将自身标记为此类状态。但我不知道在哪里查找有关此规则的信息。 - Jon Jay Obermark
@Jon,这不是与页面加载相关的问题。页面已经加载完成,用户要求执行一个占用CPU较大的任务。下面是Kevin的可行解决方案。 - pcormier
“你可能认为这个问题有一个简单的答案。”真没错。这个问题一直让我发疯。 - Mars
4个回答

19

将其设置为处理中,然后使用setTimeout来防止高CPU密集型任务在更新div之前运行。

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" ></script>
<script>
function addSecs(d, s) {return new Date(d.valueOf()+s*1000);}
function doRun() {
    document.getElementById('msg').innerHTML = 'Processing JS...';
    setTimeout(function(){
         start = new Date();
         end = addSecs(start,5);
         do {start = new Date();} while (end-start > 0);
         document.getElementById('msg').innerHTML = 'Finished Processing';   
    },10);
}
$(function() {
    $('button').click(doRun);
});    
</script>
    </head>
<body>
    <div id="msg">Not Started</div>
    <button>jQuery</button>
    <a href="#" onclick="doRun()">javascript</a>
</body>
</html>

你可以根据需要修改setTimeout的延迟时间,对于运行速度较慢的机器或浏览器可能需要调大。

编辑:

你也可以使用警报或确认对话框来允许页面有足够时间更新。

document.getElementById('msg').innerHTML = 'Processing JS...';
if ( confirm( "This task may take several seconds. Do you wish to continue?" ) ) {
     // run code here
}

5
-1是因为这实际上是一种竞态条件。即使在快速的计算机上,经过的时间也不能保证低于10毫秒... - yerforkferchips
8
是的,由于事件循环的工作方式,它确实可以保证这一点。我们只需要将回调推入回调队列中,以便在下次事件循环运行时才运行它。即使它只有0毫秒的延迟,在任何地方都可以正常工作,因为渲染器(也会被放入回调队列中)具有优先级。 - Kevin B
1
@Brett84c,同步执行并不会改变任何事情。它会立即改变DOM,但在调用栈清除之前,UI不会更新。UI将在长时间的CPU密集型任务之后才会更新。 - Kevin B
1
好的,不,DOM 属性和属性更新是同步的。它们只是不会同步渲染 - Kevin B
2
没错。当你执行 DOM 更新后跟着一个 alert('foo'),你会观察到同样的情况。弹出框会先出现,然后在你关闭弹出框之后 DOM 更新才会渲染。这是因为弹出框是同步的,就像上面那个需要大量 CPU 资源的任务一样。 - Kevin B
显示剩余9条评论

3

您有一个持续 5 秒并在此期间冻结 Web 浏览器的循环。由于 Web 浏览器被冻结,它无法进行任何渲染。您应该使用 setTimeout() 而不是循环,但我假设该循环只是替换了需要一段时间的 CPU 密集型函数?您可以使用 setTimeout 在执行函数之前给浏览器一个渲染的机会:

jQuery:

$(function() {
    $('button').click(function(){
        (function(cont){
            $('div').text('Processing JQ...');  
            start = new Date();
            end = addSecs(start,5);
            setTimeout(cont, 1);
        })(function(){
            do {start = new Date();} while (end-start > 0);
            $('div').text('Finished JQ');   
        })
    });
});

纯JS:

document.getElementsByTagName('a')[0].onclick = function(){
    doRun(function(){
         do {start = new Date();} while (end-start > 0);
         document.getElementById('msg').innerHTML = 'Finished JS';   
    });
    return false;
};

function doRun(cont){
    document.getElementById('msg').innerHTML = 'Processing JS...';
    start = new Date();
    end = addSecs(start,5);
    setTimeout(cont, 1);
}

在编写代码时,您需要始终记住使用var关键字声明所有变量,并避免将它们暴露到全局作用域中。这里有一个JSFiddle:

http://jsfiddle.net/Paulpro/ypQ6m/


3
是的,这个循环只是用来代表需要大量 CPU 运算的功能。这并不是生产代码的代表,只是一个快速演示问题的片段。 - pcormier

3
截至2019年,使用双重requesAnimationFrame来跳过一帧,而不是使用setTimeout创建竞争条件。"最初的回答"
....
function doRun() {
    document.getElementById('msg').innerHTML = 'Processing JS...';
    requestAnimationFrame(() =>
    requestAnimationFrame(function(){
         start = new Date();
         end = addSecs(start,5);
         do {start = new Date();} while (end-start > 0);
         document.getElementById('msg').innerHTML = 'Finished Processing';   
    }))
}
...

这让我感到困惑。 - TangMonk
@TangMonk,你能说一下缺少什么,这样我就可以更新这个内容——例如,有什么让你感到困惑的吗? - Estradiaz
@Estradiaz,也许您可以解释一下为什么需要双重/嵌套的requestAnimationFrame。 - run_the_race
@run_the_race 第一个回调将在下一帧中被调用,第二个回调将在一帧后被调用。因此,您可以跳过一帧,先让浏览器绘制,然后再进行操作。 - Estradiaz
@Estradiaz,非常感谢您的详细解释,但如果您现在开始做一些操作(比如在回调函数requestAnimationFrame之前),那么当第一个requestAnimationFrame触发时,这些操作和变更应该已经反映出来了吧? - run_the_race
window.requestAnimationFrame() 方法告诉浏览器您希望执行动画,并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法接受一个回调作为参数,在重绘之前被调用。详见:https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame - Estradiaz

3

我必须等待jQuery操纵DOM,然后获取更改(将表单字段多次加载到表单中,然后提交)。DOM变得越来越大,插入需要的时间也越来越长。我使用ajax保存更改,以便用户能够在离开时继续。

这种方法并没有按照预期工作:

jQuery('.parentEl').prepend('<p>newContent</p>');
doSave(); // wrapping it into timeout was to short as well sometimes

由于像 .prepend() 这样的 jQuery 函数只有在完成后才会继续链式操作,因此以下代码似乎可以解决问题:

jQuery('.parentEl').prepend('<p>newContent</p>').queue(function() {
  doSave();
});

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