重現問題
我們現在編寫一個示例來重現一個異步刷信的問題
首先我們建立一個名為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正在異步地加載腳本文件這正是產生問題的主要原因
From:http://tw.wingwit.com/Article/program/ASP/201311/21769.html