作者:Dave Massy
2001 年 2 月 26 日发布 将于 2001 年 3 月 26 日存档
上个月我们讨论了使用 DHTML 提供 Web 服务,在接下来的几个月里,我们将进一步详细讨论这个主题。在 Web 服务这个问题上,越是思考它将为 Web 应用程序提供的种种可能,我就越感到兴奋。我知道这不太好,可这些事情确实让我兴奋。不过,这个月我想我该讲讲更基本的主题。近几个月来我卷入了一些关于这个问题的漫长的电子邮件讨论,我想解释一下这个问题为什么会存在,以及可能的解决方案。这会非常有趣。引起讨论的第一封电子邮件一般是这样写的:
我编了一个使用 while 循环的脚本函数,它的执行时间很长。尽管我在循环里加入了一个作为迭代进程的更新显示的语句,可是总要到例程完成之后才会更新显示。
邮件中往往附着这样一段脚本:
<SCRIPT>
function longtime()
{
var i=0;
while (i<=500)
{
d1.innerHTML="Count ="+i;
i++;
// 以下语句代表一个很长的过程
var j=0;
while (j<=10000)
j++;
}
}
</SCRIPT>
<INPUT TYPE="BUTTON" onclick="longtime();" value="Click Me">
<DIV id="d1">This is where the count of iterations should display</DIV>
查看示例。
编写 DHTML 的新手遇到这种问题并不奇怪。尽管编过一段时间 DHTML 的人会立刻回答:“这还不明显吗?”这个问题确实值得好好解释一番。
这段 HTML 和脚本的预期效果是在执行长时间迭代操作的过程中显示进度计数。而实际的效果却非常糟糕。最明显的问题是,按下按钮后会有一个停顿,用户会看到提示,说脚本将运行很长时间,并询问是否放弃操作。同时,显示会更新为计数值。您可能会注意到出现提示之前的另一个细微效果:按钮保持按下状态,而用户根本无法使用浏览器。正是这一事实(用户被锁在浏览器之外)导致 Microsoft® Internet Explorer 对用户发出提示,以便用户能够避免运行时间过长的脚本。
这是为什么?
脚本在 Internet Explorer 中运行的一个原则是:它的运行是不可中断的。所以,执行上面的脚本时,浏览器将处于锁定状态,直至脚本运行结束。乍一看来,这象是一个糟糕的设计,一个缺陷。可是,如果停下来想一想:如果不这样做会怎样,您就可能会意识到,这根本不是缺陷,而是一个逻辑设计。
让我们设想一下,如果上面的脚本按照作者的意图执行,用户将会在显示器上看到计数值从 0 增长到 500。这时,每次显示更新时就会生成 onresize 和 onchange 这样的事件。脚本会立即处理这些事件吗?很可能不,因为这会对脚本运行的原函数造成影响,例如,全局变量值的变化。另一种方案是,在执行原函数时所有事件均排队等候处理,但这也会带来一系列问题,而且与预期效果所距甚远。有一点对脚本很有用处,即能够执行并决定内容应该如何在屏幕上显示,然后让所有更新同步发生,而不是在屏幕的不同部分不断改变显示。
那么,为什么会在提示放弃脚本操作时更新显示呢?这是因为对话框的显示会导致脚本转让控制权,所以显示引擎才有机会更新显示,显示当前的 DHTML 文档树。脚本转让控制权的思路非常重要,这一点类似于汽车让路给其他车辆加入车流。在浏览器中,当脚本引擎转让控制权时,允许显示引擎取得控制,这时,显示引擎查看 DHTML 文档树,并根据上次获得控制后发生的变化进行更新。这里我已经简化了浏览器的内部过程。显示引擎非常智能,能够找到显示内容中无效并需要调整的部分。这可能会导致当前显示内容的布局变化,继而强制屏幕的其他部分重新显示。这里需要注意的重点是,脚本控制权的转让允许显示更新和用户使用浏览器。所以,要使我们的示例脚本达到预期效果,需要强制脚本转让控制权。
使脚本转让控制权
要使脚本转让控制权,开发人员可以采用下面几种做法。首先,就像我们已经看到的,对话框会强制脚本转让控制权。显然,在我们这个示例里不想使用对话框,因为我们只想更新屏幕,而不想给用户任何提示。不过,对话框能够强制脚本转让控制权的事实引发了一个有关调试脚本的有趣问题。有时,开发人员会奇怪为什么一段代码不象预期的那样运行,而一种简单快捷的方法是在待查代码中插入如下代码行,以了解变量 mystuff 的值:
alert("The value of mystuff is: "+mystuff);
在大多数情况下,它工作得非常好。不过,因为警告会强制显示对话框,而脚本将转让控制权。这一转让会导致显示更新以及某些布局样式值的变化。所以,后续脚本流程可能会与脚本中不存在警告时不同。我经常使用的一种调试技巧是在浏览器的状态栏中显示变量的值(而不是使用警告):
window.status="The value of mystuff is: "+mystuff;
这样脚本的流程不会因为出现对话框而中断。
另一种强制脚本转让控制权的方法是使用超时,在我们的示例脚本中将使用这一技巧。
超时
超时并不仅仅是美式足球中使用的一个奇怪概念。(在美式足球中,超时允许一个队中断比赛。)它还是一种强有力的脚本编写技巧,允许在经过一段时间后再调用脚本。可用的计时器方法有两种:setTimeout(英文)和 setInterval(英文)。它们的功能类似,setTimeout 提供对函数的唯一一次回调,而 setInterval 按照适当的时间间隔重复回调函数。这里我们使用 setTimeout。
<SCRIPT>
var i;
function longtime()
{
i=0;
timedIterations();
}
function timedIterations()
{
if (i<=500)
{
d1.innerHTML="Count ="+i;
// 下列语句代表需要一段时间的计算
var j=0;
while (j<=10000)
j++;
setTimeout("timedIterations();", 1);
i++;
}
}
</SCRIPT>
<INPUT TYPE="BUTTON" onclick="longtime();" value="Click Me">
<DIV id="d1">This is where the count of iterations should display</DIV>
<SCRIPT>
var i;
function longtime()
{
i=0;
timedIterations();
}
function timedIterations()
{
if (i<=1000)
{
d1.innerHTML="Count ="+i;
// 下列语句代表需要一段时间的计算
var j=0;
while (j<=10000)
j++;
setTimeout("timedIterations();", 1);
i++;
}
}
</SCRIPT>
<INPUT TYPE="BUTTON" onclick="longtime();" value="Click Me">
<DIV id="d1">This is where the count of iterations should display</DIV>
查看示例。
在上面的示例中,我们使用了原来的代码。为清楚起见,我们将其分为两个函数,其中主函数 timedIterations 使用 setTimeout 来再次调用其自身。这里 setTimeout 的频率设置为 1 毫秒,以便其能够尽快回调。不能依赖这些计时器来计算精确的毫秒数,但如果在上述示例中将超时设置为 1000 毫秒,其结果将精确到每次迭代耗时一秒。当然,您可以根据所执行的任务,自行决定如何将过程划分为片断来进行计时迭代。
让用户等待
现在这个示例做得很好,不过,我们往往想让用户在处理过程中等待,直至处理结束。一个简单的实现方法是禁止输入并显示等待光标:
<SCRIPT>
var i;
function longtime()
{
i=0;
b1.style.cursor="wait";
b1.disabled=true;
timedIterations();
}
function timedIterations()
{
if (i<=500)
{
d1.innerHTML="Count ="+i;
// 下列语句代表需要一段时间的计算
var j=0;
while (j<=10000)
j++;
setTimeout("timedIterations();", 1);
i++;
}
else
{
b1.style.cursor="";
b1.disabled=false;
}
}
</SCRIPT>
<BODY id=b1>
<INPUT id="ip1" TYPE="BUTTON" onclick="longtime();" value="Click Me">
<DIV id="d1">This is where the count of iterations should display</DIV>
</BODY>
查看示例。
在上面的示例中,我加入了一点代码来禁用整个文档内容,并在进行迭代时显示等待光标。因此,浏览器将继续更新,用户不会锁定它,但同时用户也不能操作页面和导致其他事件发生。当然,这种方法非常直接,而您可能希望禁用页面的某些部分,而用户仍然能够通过一种集成的方式来放弃操作。“别只是拦住用户!”
这个月要说的最后一点是:在编写 DHTML 功能时,重要的一点是不要将用户与浏览器隔离开来。如果整个浏览器看起来像是锁住了,会让用户感到非常不快。即使您不希望用户在执行长时间操作时更改页面上的任何内容,也不应该这样。可以在浏览器中使用某种功能给用户提供当前状态的反馈,让用户感觉良好。最后,我们对示例做了一个非常简单的修改,显示一个随迭代进度而增长的进度栏。
查看示例。
已存档的 DHTML Dude 专栏文章