jQuery 1.5正式版已在2011/1/31釋出,照例我都會寫筆記文強迫自己搞懂改版重點,不過本次適逢過年,把這件事塞在冗長的Todo Queue屁股後,便很鴕鳥地繼續瞎忙工作與生活的大小瑣事。直到前幾天網友ChaN在留言中提了"jQuery 1.5"關鍵字,我才驚覺原來有讀者等文,那就可不好再拖了(謎之聲: 怕什麼? 你不是已經做好"外出取柴,本月休息"的告示圖檔?),趁著熱血乍現,仔細看了jQuery 1.5改版重點。

(由於篇幅過長,拆成上下兩篇)

1.5最重大的改變莫過於重寫了Ajax模式,$.ajax(), $.get(), $.post()都會傳回一個jqXHR物件,把不同瀏覽器中的XMLHttpRequest物件封裝起來,對外提供統一的程式介面,如此當我們要操作XHR時,就可忽略不同瀏覽器的差異性。

重寫Ajax的同時,jQuery 1.5還加入了Deferred物件的概念,其在Mochikit和Dojo等Javascript程式庫中已被運用多時,jQuery則從1.5起也開始支援。在以前,$.ajax/get/post只能指定一個success函數接受傳回結果,一個error函數接受呼叫失敗時傳回的例外。自jQuery 1.5起,我們可以撰寫多個success事件,分別寫不同的邏輯處理Ajax的回傳結果,而更酷的是,我們甚至可以在Ajax已開始非同步呼叫後才加掛success事件,此時jQuery會偵測Ajax呼叫狀態,當結果尚未傳回,則會等到結果傳回時一併呼叫後來加掛的sccess事件;若掛上success時Ajax呼叫結果已經回傳,則sccuess事件會立即被執行。這個新特性可以戲劇化地簡化非同步程式間的協同運作,太抽象? 那用實例來解說好了:

<%@ Page Language="C#" %>
<script runat="server">
    void Page_Load(object sender, EventArgs e)
    {
        if (Request["m"] == "ajax")
        {
            //接受參數Delay一段時間再回應
            if (!string.IsNullOrEmpty(Request["d"]))
                System.Threading.Thread.Sleep(
                    int.Parse(Request["d"]));
            //傳回目前時間
            Response.ContentType = "text/plain";
            Response.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
            Response.End();
        }
    }
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Ajax Lab1</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.min.js" 
        type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            //顯示Log, 前方加註記錄時間
            function dispLog(msg) {
                $("#spnDisp").html(
                    $("#spnDisp").html() + "<li>" +
                    new Date().toTimeString().split(' ')[0] +
                    " => " + msg
                );
            }
            //以Ajax方式取得現在時間
            var ajaxCall = $.get("Lab1.aspx?m=ajax" // + "&d=3000",
            , { rnd: Math.random() }, 
            function (resp) { //success事件
                dispLog("1st Event: " + resp);
            });
            //記錄開始呼叫時間
            dispLog("Begin $.get!");
            //等待1000ms再掛上第二個處理回應的事件
            setTimeout(function () {
                ajaxCall.success(function (resp) {
                    dispLog("2nd Event: " + resp);
                });
            }, 1000);
        });
    </script>
</head>
<body>
    <span id="spnDisp"></span>
</body>
</html>

執行以上的程式碼,我們可以得到如下的結果:

  • 06:39:56 => Begin $.get!
  • 06:39:56 => 1st Event: 06:39:56.513
  • 06:39:57 => 2nd Event: 06:39:56.513

利用setTimeout延遲1秒掛上第二個success事件,當時Ajax呼叫已經完成,而第一個success事件也已經執行,因此延遲掛上的事件立即被觸發,並且順利取得剛才傳回的結果。如果我們在ASP.NET呼叫參數加上&d=3000令其強迫延遲3秒再傳回結果,可以預期setTimeout 1秒掛上第二個success事件時,Ajax呼叫還沒完成,因此當3秒後結果傳回時,第一個及第二個success事件會一起被觸發:

  • 06:48:48 => Begin $.get!
  • 06:48:51 => 1st Event: 06:48:51.634
  • 06:48:51 => 2nd Event: 06:48:51.634

以上的實驗,可驗證jQuery Ajax的新特性,允許我們為同一個呼叫加上多個success事件,而且不管掛上事件時Ajax呼叫是否已完成,success都會被執行到。但剛才不是說可以神奇地簡化非同步程式間的協同運作,在哪裡? 換一個複雜一點的例子來說明,假設我們會先後呼叫兩次Ajax,最後加總兩次得到的結果算出答案,程式要怎麼寫?

有好幾種實踐的方法: 1) 先呼叫第1次,在第1次的success事件中發動第2次呼叫,在第2次呼叫的success計算兩次的結果 2) 第1次第2次呼叫的success事件中將結果寫到指定變數,另外用setInterval定期檢查是否兩次的結果都已寫回 3) 寫一個Queue機制,可排入Ajax工作執行,機制可彙整Ajax結果並檢查Ajax工作狀態觸發相關事件。我用一個例子示範1及2的寫法,而3,與Deferred物件精神相似,我直接使用jQuery 1.5的新寫法$.when()來展示:

<%@ Page Language="C#" %>
<script runat="server">
    static Random rnd = new Random();
    void Page_Load(object sender, EventArgs e)
    {
        if (Request["m"] == "ajax")
        {
            //Delay 1到3秒,傳回1-100亂數
            Response.ContentType = "text/plain";
            System.Threading.Thread.Sleep(rnd.Next(1000, 3000));
            Response.Write(rnd.Next(1, 100));
            Response.End();
        }
    }
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Ajax Lab1</title>
    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.min.js" 
        type="text/javascript"></script>
    <script type="text/javascript">
        $(function () {
            //顯示Log, 前方加註記錄時間
            function dispLog(msg) {
                $("#spnDisp").html(
                    $("#spnDisp").html() + "<li>" +
                    new Date().toTimeString().split(' ')[0] +
                    " => " + msg
                );
            }
            //設定不要Cache
            $.ajaxSetup({ cache: false });
            var url = "Lab2.aspx?m=ajax";
 
            test1();
 
            function test1() {
                //方法1
                dispLog("Test1");
                //兩個變數用來保存兩次呼叫的結果
                var v1 = 0, v2 = 0;
                $.get(url, {},
                function (resp) {
                    v1 = parseInt(resp);
                    dispLog("v1=" + v1);
                    $.get(url, {}, function (resp2) {
                        v2 = parseInt(resp2);
                        dispLog("v2=" + v2);
                        //最後結果
                        dispLog("v1+v2=" + (v1 + v2));
                    });
                });
            }
            function test2() {
                //方法2
                dispLog("Test2");
                var tv1 = null, tv2 = null;
                $.get(url, {}, function (resp) {
                    tv1 = parseInt(resp);
                    dispLog("tv1=" + tv1);
                });
                $.get(url, {}, function (resp) {
                    tv2 = parseInt(resp);
                    dispLog("tv2=" + tv2);
                });
                //利用輪詢檢查兩次呼叫是否都已完成
                var hnd = setInterval(function () {
                    if (tv1 != null && tv2 != null) {
                        dispLog("tv1+tv2=" + (tv1 + tv2));
                        clearInterval(hnd);
                    }
                }, 200);
            }
            function test3() {
                //Deferred
                dispLog("Test3");
                var dv1 = null, dv2 = null;
                var ajax1 = $.get(url, {}, function (resp) {
                    dv1 = parseInt(resp);
                    dispLog("dv1=" + dv1);
                });
                var ajax2 = $.get(url, {}, function (resp) {
                    dv2 = parseInt(resp);
                    dispLog("dv2=" + dv2);
                });
                $.when(ajax1, ajax2) //用when排入ajax1, ajax2所傳回的物件
                .done(function () { //ajax1或ajax2都成功能會呼叫done
                    dispLog("dv1+dv2=" + (dv1 + dv2));
                })
                .fail(function () { //ajax1或ajax2任一者失敗時執行fail
                }); //或用.then(doneFunc, failFunc)一次設定兩種結果函數
            }
        });
    </script>
</head>
<body><span id="spnDisp"></span></body>
</html>

經測試,三者都可以得到正確結果:

  • 07:35:38 => Test1
  • 07:35:41 => v1=61
  • 07:35:43 => v2=38
  • 07:35:43 => v1+v2=99
  • 07:43:47 => Test2
  • 07:43:50 => tv2=2
  • 07:43:50 => tv1=20
  • 07:43:50 => tv1+tv2=22
  • 07:49:23 => Test3
  • 07:49:25 => dv1=76
  • 07:49:25 => dv2=63
  • 07:49:25 => dv1+dv2=139

比較三種做法: 在方法1,下一次的呼叫方法必須藏進前一次呼叫的success裡,若串上四層,光程式碼內縮排版就包準讓你頭暈,更不用說會變成單執行緒的缺點。方法2用了輪詢(Polling),嚴格來說不是有效率的做法,在江湖上常被視為雞鳴狗盜之技(但坦白說我還挺愛用的,不用花太多腦筋,簡單有效立竿見影),而且Closure加clearInterval的玩法挺Tricky。至於方法3就是jQuery 1.5透過Deferred物件實現非同步程序同步的新做法,與前兩種做法相比直覺簡潔很多,我只有一句話: 我愛死它了!

Deferred物件的觀念不只應用在Ajax呼叫上,我們還可以用來簡化複雜的非同步協同作業,有興趣的朋友可以參考jQuery API文件及Eric Hynds的介紹文


Comments

# by 鐵衛

感謝黑大的犧牲奉獻,容小弟在這邊插個頭香膜拜一下 Orz

# by ChaN

感謝黑暗大滿足粉絲的需求 囧

# by jocosn

黑大,我一直覺的你的部落格技術文章寫的很好。 唯一美中不足的是,右邊 sidebar 寬度好長,左邊程式碼時常會被擠壓。美中不足。

# by Jeffrey

to jacosn, 目前部落格是採用Community Server預設版型之一為藍本調整出來,分配上是左600px, 右200px。當時600px的寬度還有考量到若讀者使用Google Reader之類訂閱工具直接開啟時,排除左方清單佔用的空間,閱讀視窗也差不多剩下這個寬度,維持在600px以下可以避免左右捲動。不過十分謝謝你的意見,我會納入未來改版的考量。

# by 低溫烘培

黑大,關於jocosn的問題,我覺得你可以直接用JQuery寫個小片斷的按鈕程式,按了之後,右邊的sidebar自動縮起來就好了。不過我不確定你左邊程式碼部分是不是會自動擴展就是了。

# by 小和

感謝黑大的分享~~ 請問我是否可以將您範例放在我網站上,我會註明程式是來自黑大網站~~

# by Jeffrey

to 小和,本部落格的文章都歡迎引用,註明出處即可。

Post a comment