[轉帖]AjaxPro 內部機制探討

再轉一帖lhdyesok的文章:

應當承認我這人實在算不上弄潮兒,Ajax 早已流行得一塌糊塗,我卻始終沒有來研究一下這個東東。上次做網站的時候,BOSS 就跟我講過,可以參考一下 Ajax 的技術,我嘴上答應,心裏卻不是十分的在乎。究其原因,一來是我這人比較固步自封,二來起初確實也沒太相信 Ajax 真有 BOSS 說的那麼神奇。

  轉變是從昨天天始的,這一週在公司主要精力都是在用 C++ 寫 framework,不得不承認它比較辛苦,細枝末節之處非常之煩,昨天下午呆着呆着就不想幹活了,就開始四處遊蕩,正好看到我們自己也有項目已經成功應用了 Ajax,於是也就想看一看,無奈那幫傢伙的開發文檔是出奇的少,只好在網上找找資料,自己研究研究吧。

  作爲一個技術人員,我看到一項新技術,總是喜歡琢磨琢磨它內部是如何實現的。在對 Ajax 有了初步認識以後,自然想看看其內部機制,但是令我失望的是,至少介紹 Ajax 內部實現的文章少之又少,好容易找到一篇,卻也只是簡單列了列一些 javascript 代碼,並且沒什麼解釋,頗爲鬱悶。想想求人不如求己,況且自己研究的或許印象更深一些。於是找到了一個 AjaxPro,下來琢磨琢磨,只是對於 JavaScript 我實在知之甚少,不明白之處依然很多,不過還是想寫出來,拋磚引玉,望高人們不吝指教。

  一、使用的例子

  本文使用的例子很簡單,一個文本框,在其中敲入文字之後,下方就顯示該文字並加上一個“(Hello from server)”。源碼如下(有刪節):

 1<%@ Page language="c#" ClassName="KeyPressDemo" Inherits="System.Web.UI.Page" %>
 2<script runat="server" language="c#">
 3
 4private void Page_Load(object sender, EventArgs e)
 5{
 6    AjaxPro.Utility.RegisterTypeForAjax(typeof(KeyPressDemo));
 7}

 8
 9[AjaxPro.AjaxMethod]
10public string EchoInput(string s)
11{
12    return s += " (Hello from server)";
13}

14
15
</script>
16
17<form id="Form1" method="post" runat="server"></form>
18
19<div class="content">
20<h1>KeyPressDemo Examples</h1>
21<p>Press any key in the textbox and see the echo in the DIV element on the right side.</p>
22<input type="text" id="myinput" onkeyup="doTest1();"/> <div id="mydisplay">---- empty ----</div>
23<p><i>Note, that I do not update the display if a request is running currently.</i></p>
24</div>
25
26<script type="text/javascript" defer="defer">
27
28var timer = null;
29
30function doTest1() {
31    if(timer != null{
32      clearTimeout(timer);
33    }

34    timer = setTimeout(doTest1_next, 100);
35}

36
37function doTest1_next() {
38    var ele = document.getElementById("myinput");
39    ASP.KeyPressDemo.EchoInput(ele.value, doTest1_callback);
40}

41
42function doTest1_callback(res) {
43    var ele = document.getElementById("mydisplay");
44    ele.innerHTML = res.value;
45}

46
47
</script>


  選用這個例子,是因爲它比較簡單,沒有相關的 C# 文件。首先看看第17行,咦?怎麼這個 form 裏啥都沒有?既然什麼都沒有?刪掉它行不行?不行,絕對不行!看看網頁打開後,這一行被擴展成了什麼?

1<form name="Form1" method="post" action="keypress.aspx" id="Form1">
2<div>
3<script type="text/javascript" src="/ajaxdemo/ajaxpro/core.ashx"></script>
4<script type="text/javascript" src="/ajaxdemo/ajaxpro/ASP.KeyPressDemo,App_Web_vxhzzzxr.ashx"></script>
5</form>
6


  請注意這裏鏈入的兩個 javascript 文件,它們是 Ajax 得以運行的基礎!刪掉 form 那一行,它們就不會出現,自然不行了。這兩行是如何產生的?那就是頁面代碼的第4至7行的 PageLoad 函數的功勞了。

  好,那這兩個 javascript 文件我們能看到不?看上去它們是服務端的,並且事實上是服務端動態生成的。不過稍有些瞭解瀏覽器工作原理的人就會知道,到 Local Settings 下的 Temporary Internet Files 目錄下去找,肯定是有的,因爲瀏覽器下載頁面的時候會把與頁面相關的文件都下過來。

  二、Ajax ClientScript 的執行總體流程

  好,有了源頁面代碼,又有了兩個 ClientScript 文件,我們就可以分析客戶端的執行流程了。以下是我畫的一張簡單的流程圖:



  我們一個一個地來分析。

  三、HTML頁面做了什麼?

  第一步,當我們在 TextBox 裏輸入字符後,將會觸發 onkeyup 事件。它要執行 doTest1 方法。見頁面代碼裏的第22行。

  第二步,doTest1 方法使用 setTimeout 函數,設定了 100 毫秒後,執行 doTest1_next 方法。見頁面代碼裏的第34行。

  第三步,doTest1_next 方法調用了 ASP.KeyPressDemo.EchoInput 方法,它帶有兩個參數,第一個是我們在文本框中輸入的值,當然是個字符串類型的了;第二個則是一個 callback 函數,請留心這個函數,它將於整個流程的最後執行。

  好,我們知道頁面的客戶端無外乎就是 HTML 和 JavaScript,雖然 ASP.KeyPressDemo.EchoInput 方法酷似頁面裏我們自己用 C# 寫的函數,但可以肯定的是它絕對是用 JavaScript 實現的。在哪兒呢?嗯,在我們從 Temporary Internet Files 目錄下找到的 ASP.KeyPressDemo,App_Web_vxhzzzxr.ashx 裏。

  四、ASP.KeyPressDemo,App_Web_vxhzzzxr.ashx 的實現

  這個文件很小,以下是它的全部源碼:

 1addNamespace("ASP");
 2ASP.KeyPressDemo_class = Class.create();
 3ASP.KeyPressDemo_class.prototype = (new AjaxPro.Request()).extend({
 4    EchoInput: function(s, callback) {
 5        return this.invoke("EchoInput"{"s":s}, callback);
 6    }
,
 7    initialize: function() {
 8        this.url = "/ajaxdemo/ajaxpro/ASP.KeyPressDemo,App_Web_vxhzzzxr.ashx";
 9    }

10}
)
11ASP.KeyPressDemo = new ASP.KeyPressDemo_class();
12


  啊哈,這下我們知道了,ASP.KeyPressDemo 其實是在這裏用 JavaScript 定義的 ASP.KeyPressDemo_class 類的實例,EchoInput 則是它的一個方法。注意一下每3行,我們看到這個類是從 AjaxPro.Request 類繼承的。什麼什麼?繼承?有沒有搞錯?JavaScript 什麼時候開始面向對象了而不是基於對象了?先擺下這個疑問,我們繼續往下看。

  EchoInput 方法的實現很簡單,就是調用了一個 Invoke 方法。嗯,這個方法想必是從 AjaxPro.Request 類“繼承”下來的。那它定義在哪兒?是了,還有一個 core.ashx 呢,它纔是真正客戶端實現 Ajax 技術的主角!這個文件太大,我們還是依照函數調用順序慢慢來拆解罷。

  五、Invoke 函數

  Invoke 函數是核心所在,前面我畫的流程圖中已經簡單地描述了它的主要流程。不過這個函數太重要了,這裏還是列出它的全部源碼:

 1AjaxPro.Request = Class.create();
 2AjaxPro.Request.prototype = (new AjaxPro.Base()).extend({
 3    invoke: function(method, data, callback) {
 4        var async = typeof callback == "function" && callback != AjaxPro.noOperation;
 5        var json = AjaxPro.toJSON(data) + "/r/n";
 6
 7        if(AjaxPro.cryptProvider != null)
 8            json = AjaxPro.cryptProvider.encrypt(json);
 9
10        this.callback = callback;
11
12        if(async) {
13            this.xmlHttp.onreadystatechange = this.doStateChange.bind(this);
14            if(typeof this.onLoading == "function"this.onLoading(true);
15        }

16            
17        this.xmlHttp.open("POST"this.url, async);
18        this.xmlHttp.setRequestHeader("Content-Type""application/x-www-form-urlencoded");
19        this.xmlHttp.setRequestHeader("Content-Length", json.length);
20        this.xmlHttp.setRequestHeader("Ajax-method", method);
21        
22        if(AjaxPro.token != null && AjaxPro.token.length > 0)
23            this.xmlHttp.setRequestHeader("Ajax-token", AjaxPro.token);
24
25        if(MS.Browser.isIE)
26            this.xmlHttp.setRequestHeader("Accept-Encoding""gzip, deflate");
27        else
28            this.xmlHttp.setRequestHeader("Connection""close");    // Mozilla Bug #246651
29
30        if(this.onTimeout != null && typeof this.onTimeout == "function")
31            this.timeoutTimer = setTimeout(this.timeout.bind(this), this.timeoutPeriod);
32
33        this.xmlHttp.send(json);
34        
35        json = null;
36        data = null;
37        delete json;
38        delete data;
39        
40        if(!async) {
41            return this.createResponse();
42        }

43        
44        return true;    
45    }

46}
);
47


  嗯,相當複雜啊。我們慢慢地看。

  AjaxPro.Request 類當然不是隻有 Invoke 一個函數,這裏省去了其它函數。嗯,我們看到,AjaxPro.Request 也是從 AjaxPro.Base “繼承”下來的。

  第4行的 async,字面上理解就是指異步,這一行什麼意思?嗯,如果傳進來的 callback 類型是函數,並且不是無操作,那就認爲是異步的。

  第5行的 json,它可是相當重要啊。這裏調用了 AjaxPro.toJSON 方法把傳進來的數據進行了某種編碼,本例中這個數據當然就是從 doTest1_next 一路傳進來的 TextBox 裏我們輸入的字符串值了,這個函數的實現,本文也不再列出,可以參見 core.ashx 文件。

  接下來第7到8行,如果提供了加密,那麼就對 json 進行加密。這個好理解。

  第12到15行,如果是異步的,那麼這裏將 doStateChange 函數綁定到 onreadystatechange 事件上去。嗯,這裏的綁定其實也是在 core.ashx 文件裏聲明的一個方法,本文不再闡述它的實現了,大家有興趣,可以自己去看。綁定完成後,當服務端完成操作後,doStateChange 函數會被調用,這時可以進行更改頁面的工作。此外,這裏還檢測了一下 onLoading 事件。

  第17行到第33行可謂核心代碼,我們知道 Ajax 就是使用的 XMLHttpRequest 來完成無刷新頁面的。這裏我們可看到 this.xmlHttp 被用來進行了請求封裝。其中值得我們注意的,Content-Length 使用的 json.length,Ajax-method 則使用的就是傳進來的 AjaxMethod 方法名稱,本例中爲 EchoInput。第30、31行設置了超時處理,當然了,頁面不能死等嘛。第33行則將 json 發送到服務端。

  接下來的第41行,我們看到如果不是異步操作的話,此處將直接調用 createResponse 函數獲得響應。那如果是異步操作呢?記得我們設置了 doStateChange 吧?異步的返回處理就是它的事了。createResponse 函數後面再介紹。

  六、解釋“繼承”

  前面我們好幾次看到貌似繼承。當然它們都僅僅是貌似而已。看看以下 core.ashx 中的代碼就明白了: 

1Object.extend = function(destination, source) {
2    for(property in source) {
3        destination[property] = source[property];
4    }

5    return destination;
6}

7

  哈哈,所謂的“繼承”,其實只是個屬性拷貝而已。

  七、this.xmlHttp 從何而來?

  前面我們看到了 this.xmlHttp 大顯神威。那麼它是哪兒來的?看看 AjaxPro.Request 類的 initialize 函數吧(有刪節):

1initialize: function(url) {
2    this.xmlHttp = new XMLHttpRequest();
3}

4

  是了,xmlHttp 只是 XMLHttpRequest 的一個實例。那麼 XMLHttpRequest 的定義呢?

 1var lastclsid = null;
 2if(!window.XMLHttpRequest) {
 3
 4    function getXmlHttp(clsid) {
 5        var xmlHttp = null;
 6        try {
 7            xmlHttp = new ActiveXObject(clsid);
 8            lastclsid = clsid;
 9            return xmlHttp;
10        }
 catch(ex) {}
11    }

12    
13    window.XMLHttpRequest = function() {
14        if(lastclsid != null{
15            return getXmlHttp(lastclsid);
16        }

17    
18        var xmlHttp = null;
19        var clsids = ["Msxml2.XMLHTTP.6.0","Msxml2.XMLHTTP.5.0","Msxml2.XMLHTTP.4.0","Msxml2.XMLHTTP.3.0","Msxml2.XMLHTTP.2.6","Microsoft.XMLHTTP.1.0","Microsoft.XMLHTTP.1","Microsoft.XMLHTTP"];
20
21        for(var i=0; i<clsids.length && xmlHttp == null; i++{
22            xmlHttp = getXmlHttp(clsids[i]);
23        }

24        
25        if(xmlHttp == null{
26            return new IFrameXmlHttp();
27        }

28
29        return xmlHttp;
30    }

31}

32

  哦,原來是在這裏真正創建的。說到底還是一個 ActiveXObject 啊。關於這個本文也不再多提。不過代碼中還需要注意的一點是,
如果把第19行列出的一大堆clsids 都處理過了還沒有得到對象怎麼辦?注意到第26行 new 了一個 IFrameXmlHttp。

  IFrameHttp 是在 core.ashx 中定義的,它基本上完全模擬了 ActiveXObject 對象的功能。想研究研究的,自己看源碼吧。篇幅所限,這裏不多講啦。

  八、doStateChange 函數

  嗯,前面已經提過,異步的話 doStateChange 函數將會在服務端返回後執行,看看它的源碼呢:

 1doStateChange: function() {
 2    if(this.onStateChanged != null && typeof this.onStateChanged == "function")
 3        trythis.onStateChanged(this.xmlHttp.readyState); }catch(e){}
 4
 5    if(this.xmlHttp.readyState != 4)
 6        return;
 7
 8    if(this.xmlHttp.status == 200{
 9        if(this.timeoutTimer != null) clearTimeout(this.timeoutTimer);
10        if(typeof this.onLoading == "function"this.onLoading(false);
11
12        this.xmlHttp.onreadystatechange = AjaxPro.noOperation;
13
14        this.callback(this.createResponse());
15        this.callback = null;
16
17        this.xmlHttp.abort();
18    }

19}
,
20

  如果 status 是 200,也就是 OK,那麼清除掉超時處理函數,處理 onLoading 事件,最後使用 callback 調用 createResponse 函數。還記得如果不是異步的話,createResponse 將會直接調用而不是通過 doStateChange 吧。

  九、createResponse 函數

 1createResponse: function() {
 2    var r = new Object();
 3    r.error = null;
 4    r.value = null;
 5
 6    var responseText = new String(this.xmlHttp.responseText);
 7
 8    if(AjaxPro.cryptProvider != null && typeof AjaxPro.cryptProvider == "function")
 9        responseText = AjaxPro.cryptProvider.decrypt(responseText);
10
11    eval("r.value = " + responseText + ";");
12
13    if(r.error != null && this.onError != null && typeof this.onError == "function")
14        trythis.onError(r.error); }catch(e){}
15
16    responseText = null;
17
18    return r;
19}

  如果前面的 json 也就是 Request 是加過密的,這裏就需要對 responseText 進行解密。完了之後得到 r.value,r 將會被返回並提供給 callback 函數。本例中將最終傳回 doTest1_callback,r 被傳入它的 res 參數。最後更新文本框下的字符串,整個 Ajax ClientScript 的流程就差不多是完成了。

  十、簡單總結一下

  呼,長出一口氣。總算可以告一段落了,AjaxPro 服務端的拆解過段時間再說吧。

  在分析 ClientScript 端的時候真是大有感觸,JavaScript 其實遠比人們想象的強大和管用。其實我同大多數人一樣,起初也對它很不感冒,但是之前曾有兩件事讓我改變了觀念。其一是閱讀了黃忠成的《深入剖析 ASP.NET 組件設計》,才發現原來許多強大炫目的 ASP.NET 的控件,其實都是用的 JavaScript 實現。其二是在研究國外某文檔瀏覽器實現的時候,發現人家使用 JavaScript 在 IE 下相當完美地實現了強大靈活有如桌面程序的界面和功能,真是吃驚不小。當時就發現了自己對 JavaScript 的瞭解實在是嚴重汗顏,慚愧無地。無奈平時沒有多少時間去學習提高自己,只能偶爾抽抽空餘時間瞭解瞭解,充充電吧。

  相信 JavaScript 之類的腳本必將在未來的 Web 應用中大展身手。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章