Quantcast
Channel: 黑暗執行緒
Viewing all articles
Browse latest Browse all 2498

再談AJAX呼叫的同步化

$
0
0

接獲報案:

某網頁透過AJAX新增資料,接著以AJAX方式取回資料清單,卻不見剛才新增的項目,重新整理網頁則正常。

經過一番檢測,確認與AJAX的非同步特性有關,循序執行AJAX呼叫的做法先前曾討論過,但這回我們改從問題剖析的角度再切入一次。

試著用以下範例重現問題。假設有一個後端ASP.NET程式如下,傳入參數mode=add時可新增字串元素,mode=clear時清除所有資料,否則傳回字串陣列內容。為求簡化,此處用Session儲存資料取代原本的DB作業。

<%@ Page Language="C#" %>
 
<script runat="server">
protectedvoid Page_Load(object sender, EventArgs e) 
    {
//在Session中放入List<string>物件
        var list = Session["Storage"] as List<string>;
if (list == null)
        {
            list = new List<string>();
            Session["Storage"] = list;
        }
        Response.ContentType = "text/plain";
        var mode = Request["mode"];
//三種模式,add加入字串元素
if (mode == "add")
        {
            var text = Request["text"];
            list.Add(text);
            Response.Write("Added");
        } //clear清除內容
elseif (mode == "clear")
        {
            list.Clear();
            Response.Write("Cleared");
        }
else//或傳回字串內容
        {
            Response.Write(string.Join(",", list.ToArray()));
        }
        Response.End();
    }
</script>

前端網頁如下,有兩個JavaScript函式add及display,分別負責以AJAX方式呼叫上述ASPX新增資料及取回資料清單。網頁則有一個TextBox供輸入文字,一顆按鈕送出、一顆按鈕重取資料顯示於下方DIV,在新增鈕Click事件裡先呼叫add()再呼叫display(),原本預期新增後可立刻顯示最新結果。

<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<head>
<title>AJAX Sync Issue 1</title>
</head>
<body>
<inputtype="text"id="t"/><inputtype="button"id="b"value="Add"/>
<inputtype="button"id="r"value="Refresh"/>
<br/>
<divid="d"></div>
<scriptsrc="//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.9.0.min.js"></script>
<script>
        $(function () {
function add() {
                $.post(
"DataService.aspx",
                    { mode: "add", text: $("#t").val() },
function (result) {
if (result != "Added") alert(result);
                    }
                );
            }
function display() {
                $.post("DataService.aspx", {}, function (result) {
                    $("#d").text(result);
                });
            }
            display();
            $("#b").click(function () {
                add();
                display();
            });
            $("#r").click(function () { display(); });
        });
</script>
</body>
</html>

結果發生什麼事? 請看以下示範,新增1後馬上看到結果,新增2後仍然只顯示1,但新增3之後才一次冒出1,2,3。

這是AJAX初心者很容易踩到的陷阱 -- 忘記AJAX呼叫都是非同步的!

jQuery.post(), jQuery.get(), jQuery.ajax()動作都採非同步執行,意思是add()呼叫$.post()新增資料後,並不會等Server端傳回結果才繼續執行下一行指令。因此,不管新增動作完成與否,display()呼叫$.post()查詢的Request便已送出,甚至可能比新增資料的Request更早執行完畢,在此種狀況下接收到的便是尚未新增完成前的結果,不包含新增資料。

由Request時序圖可清楚看出問題所在。在下圖中,我共新增了兩次資料,每次新增都會產生兩個Request(一個mode=add,一個查結果),發現了嗎? 兩個Request幾乎是同時送出,而執行時間長短不一,第一次新增花0.758秒,顯示花0.259秒;第二次反過來,新增只花0.012秒,但查詢耗時0.510秒。當新增很快完成,查詢就能抓到最新結果;反之就會看到未新增前的狀態,足以解釋為什麼前述示範中1,3新增馬上看到結果,2新增後卻沒更新。

要解決這個問題,最簡單的方法就是強迫查詢必須在新增作業完成後再執行,將程式放進jQuery.post的success Callback函式執行,即可滿足需求。

稍加修改程式,在add函式中加入cb函式參數,並將其置入$.post()待收到Server回應後呼叫,而在呼叫add時,傳入呼叫display()的程式當成cb參數,即完成串接:

        $(function () {
function add(cb) {
                $.post(
"DataService.aspx",
                    { mode: "add", text: $("#t").val() },
function (result) {
if (result != "Added") alert(result);
if (cb) cb();
                    }
                );
            }
function display() {
                $.post("DataService.aspx", {}, function (result) {
                    $("#d").text(result);
                });
            }
            display();
            $("#b").click(function () {
                add(function () { display(); });
            });
            $("#r").click(function () { display(); });
        });

重新執行測試,我們可以看到第二個Request的執行時間一律在第一個Request結束後,解決了有時看不到剛新增資料的問題。

不過,透過Callback參數的做法有個缺點,例如: 還有工作要排在display()完成後進行,Callback層層相套會變成:

add(function() { 
  display(function() { 
//…blah… 
  }); 
});

看起來有點噁心,對吧? 該jQuery.Deferred上場了!

        $(function () {
function add() {
return $.post(
"DataService.aspx",
                    { mode: "add", text: $("#t").val() },
function (result) {
if (result != "Added") alert(result);
                    }
                );
            }
function display() {
return $.post("DataService.aspx", {}, function (result) {
                    $("#d").text(result);
                });
            }
            display();
            $("#b").click(function () {
                add().done(function () { display(); });
            });
            $("#r").click(function () { display(); });
        });

我們讓add()、display()拋回$.post()所傳回jQuery.Deferred物件,原本要當成參數的Callback便可改放在done()之中,寫成: add().done(function() { display(); });,順序接在函式之後更加直觀。如果要串接更多作業呢? 去吧!pipe() .then()就決定是你了: (延伸閱讀: 以jQuery循序執行AJAX呼叫,並依結果決定是否繼續) [2013-10-22更新: .pipe()自18.0起已列為過時,應改用then(),感謝Ammon回饋。 ]

            $("#b").click(function () {
                add()
                .then(function () { return display(); })
                .then(function () { return $.get("Lab1.html"); })
                .then(function () { return $.get("Lab2.html"); });
            });

這樣子處理同步是不是優雅多了呢?

正被命運鎖鏈一步步推向前端火坑的朋友們,建議花點時間熟悉一下jQuery.Deferred,保證值回票價。


Viewing all articles
Browse latest Browse all 2498


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>