重现问题 我们现在编写一个示例来重现一个异步刷信的问题 首先我们建立一个名为ScriptHandlerashx的Generic Handler它的作用是模拟一个脚本文件可以看出加载这么一个脚本文件是一个很耗时的操作
ScriptHandlerashx <%@ WebHandler Language=C# Class=ScriptHandler %> using System; using SystemWeb; public class ScriptHandler : IHttpHandler { public void ProcessRequest (HttpContext context) { contextResponseContentType = text/javascript; SystemThreadingThreadSleep(); contextResponseWrite(SysApplicationnotifyScriptLoaded();); } // } 然后我们创建一个简单的页面放置一个UpdatePanel和两个按钮 Page <asp:UpdatePanel ID=UpdatePanel runat=server> <ContentTemplate> <%= DateTimeNow %><br /> <asp:Button ID=Button runat=server Text=Load Script File OnClick=Button_Click /> <asp:Button ID=Button runat=server Text=Partial Rendering OnClick=Button_Click /> </ContentTemplate> </asp:UpdatePanel> 下面的代码是响应按钮Click事件的实现当我们点击Load Script File按钮时ScriptHandlerashx会被作为脚本文件添加到页面上而Partial Rendering则会发起一个需要等待很长时间的异步刷新 Event Handler protected void Button_Click(object sender EventArgs e) { ScriptManagerRegisterClientScriptInclude(thisPage thisGetType() key ScriptHandlerashx?m= + new Random(DateTimeNowMillisecond)Next()); } protected void Button_Click(object sender EventArgs e) { ThreadSleep(); } 您可以点击这里下载这个重现问题的示例并将它部署在您的机器上您也可以点击这里察看这个页面请一步一步跟着我来浏览这个页面我会示范一下这个问题 打开页面我们可以看到时间和两个按钮 点击Load Script File 按钮并等待时间更新 在时间更新后点击Partial Rendering 按钮 一般来说最后一步之后大约秒多钟时间将会被跟新但是现在您会发现直到您重新点击某个按钮之后时间才会更新事实上最后一步的任何操作例如脚本加载Hidden Field的注册都失败了客户端生命周期的事件也不会触发 原因何在? 在我分析客户端异步刷新的机制之前我想简单的解释一些JavaScript语言和DOM操作的基本特性使用JavaScript来操作页面中的DOM是AJAX技术的基础有人说JavaScript编程是没有多线程的因此我们能够认为它始终线程安全我同意这一点JavaScript的编程模型的确没有多线程的机制它是线程安全的——从理论上来说的确是这样 但是使用JavaScript进行编程还是会遇到同步问题因为有些操作是异步得尤其是在我们作一些DOM操作时在AJAX编程中最着名的异步操作自然就是XMLHttpRequest对象的send方法当我们调用了send方法之后下面的代码并不会被阻塞而是会继续执行下去我们还会遇到别的异步操作例如开发人员经常会发现他们无法在页面中动态创建了图片(<img />)或者添加了脚本文件引用(<script />)之后立即获得图片得尺寸或者执行文件中定义的方法这是因为下载图片和加载脚本文件都是异步操作在大多数情况下异步操作无法立即生效它往往会使用一些类似于回调函数的机制来通知开发人员事情已经准备好了 我们不难理解异步操作可能会带来同步性方面的问题我画了一幅示意图来展示异步刷新机制中可能存在的同步和异步操作请注意在ASPNET AJAX的设计中PageRequestManager使用了标准的Singleton模式因此在整个页面中只存在一个PRM实例这看起来还真是一个同步问题的温床 /P> http://imgeducitycn/img_///png> 这并不是一幅客户端生命周期的示意图因为我要指出问题是如何实现的因此需要表现的是异步刷新过程中的一些细节 请注意图中橙色的箭头它代表了异步操作中的等待实现它们是唯一可能造成同步问题的地方过程中其余部分不会被中断这是语言特性决定的 图中深蓝色的三个部分导致了同步问题的发生如果我说这些部分的本意是为了避免问题的发生您是否会觉得惊讶呢?让我们通过分析相关实现来看一下这三个关键步骤是如何工作的 cellPadding= width= align=center bgColor=#fff border= heihgt=> 实现 function Sys$WebForms$PageRequestManager$_onFormSubmit(evt) { // // prepare the request object var request = new SysNetWebRequest(); // // initialize request var handler = this_get_eventHandlerList()getHandler(initializeRequest); // // Step : abort the existing async postback thisabortPostBack(); // Step : replace the request object this_request = request; // invoke the request requestinvoke(); // } function Sys$WebForms$PageRequestManager$abortPostBack() { if (!this_processingRequest && this_request) { this_requestget_executor()abort(); // Step : clear the request object this_request = null; } } function Sys$WebForms$PageRequestManager$_onFormSubmitCompleted(sender eventArgs) { this_processingRequest = true; // // Step : validate the request if (!this_request || senderget_webRequest() !== this_request) { return; } // // execute and load scripts scriptLoaderloadScripts( FunctioncreateDelegate(this this_scriptsLoadComplete) null null); } function Sys$WebForms$PageRequestManager$_scriptsLoadComplete() { // // Page loaded this_pageLoaded(false); // Step : end postback this_endPostBack(null this_response); // } function Sys$WebForms$PageRequestManager$_endPostBack(error response) { this_processingRequest = false; // Step : clear the request this_request = null; // } 从上面的代码中我们可以发现这三个步骤都是基于当前异步刷新的Request对象进行的当一个新的异步刷新被发起时之前的那个异步刷新将被取消与此同时旧的Request对象将从PRM对象中除去并使用新的对象来替换它(step )在得到了服务器端的Response之后我们会检验Response的Request对象是否为PRM对象上的那个如果两个Request对象并不是同一个则表示获得的Response对象并不是当前的Request对象所对应的那个我们则会将其直接丢弃(step )在异步刷新结束之后PRM对象上的Request对象则会被去除(step ) 下面的示意图向您展示了用户连续发出两个异步请求时的状况 http://imgeducitycn/img_///png> 这是用户在前一个异步刷新等待服务器端回应时发起第二个异步刷新的情况那么如果一个信息的异步刷新请求在前一个正在加载脚本文件时被发起了又会出现什么状况呢?我们可以通过下一幅示意图来观察这个状况 http://imgeducitycn/img_///png> 第二个请求在第一次异步刷新加载脚本时发起如果在第二次请求得到服务器端的结果之前脚本文件加载完成则PRM对象上的Request对象就被去除了——即时目前的对象并不属于第一次异步刷新这时当第二次异步刷新得到服务器端的回应之后PRM就会立即将它丢弃因为Request对象已经不存在了 如何避免 新的异步刷新被取消了是吗?并非如此如果一个异步请求被取消的话endRequest事件将会被触发但是新的异步刷新在我们无法控制的情况下被中断了由于客户端生命周期中的事件无法被触发开发人员设计的一些逻辑也有可能会被中断我们究竟该如何防止这样的情况出现呢?幸运的是我们很容易做到这一点 优化脚本加载时间 避免一个已经发起的异步刷新被取消了 避免在PRM的_processingRequest变量为true的时候取消一个异步刷新 其中的最后一点可能还需要再多解释一下PRM对象上的_processingRequest变量会在收到服务器端回应时被设为true并且在整个异步刷新过程结束时设为false如果您的代码发现这个值为true的话就必须当心了由于此时PRM正在异步地加载脚本文件这正是产生问题的主要原因 |