再談AJAX呼叫的同步化

接獲報案:

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

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

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

<%@ Page Language="C#" %>
 
<script runat="server">
    protected void 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清除內容
        else if (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(),原本預期新增後可立刻顯示最新結果。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>AJAX Sync Issue 1</title>
</head>
<body>
    <input type="text" id="t" /><input type="button" id="b" value="Add" />
    <input type="button" id="r" value="Refresh" />
    <br />
    <div id="d"></div>
    <script src="//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,保證值回票價。

歡迎推文分享:
Published 17 October 2013 05:14 PM 由 Jeffrey
Filed under: ,
Views: 33,034



意見

# Phoenix said on 17 October, 2013 08:09 AM

原來pipe是這樣用,我之前用then,結果第一個執行完,後面的一串都同時執行@@

# 小黑 said on 17 October, 2013 08:29 PM

這篇文實在太棒了,謝謝黑大;

另外想請教一下,文中觀看 Request時序圖 的工具是甚麼?

# Jntty said on 17 October, 2013 10:24 PM

function display() {

     //略

}

display();

想請問一下,為什麼要寫一次display();

如果沒有這行會怎樣嗎?

這概念不太通,謝謝!

# Jeffrey said on 18 October, 2013 10:38 AM

to 小黑,工具是HttpWatch http://www.httpwatch.com/

to Jntty, 指網頁載入時立刻呼叫一次,才會在一開始時<div>就有內容,否則要等到使用按鈕才能看到。

# Ammon said on 22 October, 2013 02:43 AM

.pipe 已被標記為過時,取而代之的是 .then

# Jeffrey said on 22 October, 2013 10:03 AM

to Ammon, 感謝指正,已補充於本文。

# Leo said on 22 October, 2013 11:25 PM

黑大,我有一點小建議,在你這個案例,應該是不需要使用deferred,似乎在你ajax的function裡面加上 async: false 應該就可以解決這個問題了

# Jeffrey said on 23 October, 2013 12:18 AM

to Leo, async: false會導致在Server傳回結果前網頁鎖死完全無法操作,為使用者帶來不好的感受,在設計上我會極力避免。

# -_- said on 14 April, 2014 06:10 AM

我是來找jquery post/get的非同步方法。

地方是找到了,但是看了幾遍都不知道你的程式在寫什麽。

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<October 2013>
SunMonTueWedThuFriSat
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication