再談AJAX呼叫的同步化
| | 10 | | 39,016 |
接獲報案:
某網頁透過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,保證值回票價。
Comments
# by Phoenix
原來pipe是這樣用,我之前用then,結果第一個執行完,後面的一串都同時執行@@
# by 小黑
這篇文實在太棒了,謝謝黑大; 另外想請教一下,文中觀看 Request時序圖 的工具是甚麼?
# by Jntty
function display() { //略 } display(); 想請問一下,為什麼要寫一次display(); 如果沒有這行會怎樣嗎? 這概念不太通,謝謝!
# by Jeffrey
to 小黑,工具是HttpWatch http://www.httpwatch.com/ to Jntty, 指網頁載入時立刻呼叫一次,才會在一開始時<div>就有內容,否則要等到使用按鈕才能看到。
# by Ammon
.pipe 已被標記為過時,取而代之的是 .then
# by Jeffrey
to Ammon, 感謝指正,已補充於本文。
# by Leo
黑大,我有一點小建議,在你這個案例,應該是不需要使用deferred,似乎在你ajax的function裡面加上 async: false 應該就可以解決這個問題了
# by Jeffrey
to Leo, async: false會導致在Server傳回結果前網頁鎖死完全無法操作,為使用者帶來不好的感受,在設計上我會極力避免。
# by -_-
我是來找jquery post/get的非同步方法。 地方是找到了,但是看了幾遍都不知道你的程式在寫什麽。
# by 康
謝謝黑大&Leo 我的網頁一直以來都是用async:false所以總是被鎖死 看完這篇終於知道為什麼了!!!! 謝謝兩位!!!! 我馬上來改XD