這篇博文是我針對項目組開發中遇到的問題研究,今天已經和同事們進行了分享,這里把它貼到我的博客里,和廣大博友交流,希望能在和大家交流中自己得到進一步的提高。
和同事交流的文檔的標題是:關於javascript的回調函數及ajax回調函數研究
具體內容如下:
1.1開發中遇到的問題
最近開發中我和同事都碰到這樣的問題,我們使用jQuery的ajax方法做服務端的校驗,在success方法里將驗證結果存儲到一個js的公共變量或者是頁面里的隱藏域,接下來的代碼我們會根據這個公共的js變量或者是這個隱藏域里的值判斷下一步的操作,但是這樣做的結果很讓人失望,我們發現js公共變量的值或者是隱藏域的值並沒有改變,從而導致我們下面的代碼無法正常運行。下面我模擬這個問題產生的代碼,代碼如下:
callback.js:
 
          
         View Code  
         <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>回調函數 CallBack Function Study</title> </head> <script type="text/javascript" src="jquery-1.7.1.js"></script> <body> <form> <label for="txt"> 文本框: </label> <input type="text" id="txt" name="txt" size="32"/>    <input type="button" id='btn01' name='btn01' value='BUTTON01'/> </form> </body> </html> <script type="text/javascript"> var outerdata = '00'; $(document).ready(function(){ $('#txt').val('000000');//給文本框初始值 $('#btn01').bind('click',function(){ $.ajax({ type: "POST", url: "<%=request.getContextPath() %>/vumssmer/vmerservice!studyCallBack.do", data:'', success:function(msg){ console.log('msg.vflag:' + msg.vflag); console.log('msg.vmsg:' + msg.vmsg); $('#txt').val(msg.vflag); outerdata = msg.vflag; } }); if (outerdata == 'true'){ console.log('文本框內容是:' + $('#txt').val()); console.log('公共變量的值是:' + outerdata); console.log('yes'); }else{ console.log('文本框內容是:' + $('#txt').val()); console.log('公共變量的值是:' + outerdata); console.log('no'); } }); }); </script>
java程序:
public String studyCallBack() throws Exception{ this.vflag = "true"; this.vmsg = "Number:9999999"; return "validateServerBack"; }
Struts的配置文件:
<package name="vumssmer" extends="json-default" namespace="/vumssmer"> <action name="vmerservice" class="com.unionpay.mim.web.action.UMSSMerManageAction"> <result name="validateServerBack" type="json"> <param name="includeProperties">vmsg,vflag</param> </result> </action> </package>
【注意:console.log方法只有在firebug里使用才有效】
執行結果是,如圖1-1:

圖1-1
服務端我設定的返回值vflag:true,vmsg:Number:9999999,success方法打印的結果是正確,但是接下來的代碼卻執行錯誤了。
以上就是我們在開發過程中遇到的問題,下面我會從這個現象一步步研究,希望最終的結論與正確的答案一致。
1.2研究“開發中遇到問題”的過程
我首先把btn01的click事件拆分為兩個獨立的按鈕事件,大家看callback.jsp的代碼:
 
          
         View Code  
         <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>回調函數 CallBack Function Study</title> </head> <script type="text/javascript" src="jquery-1.7.1.js"></script> <body> <form> <label for="txt"> 文本框: </label> <input type="text" id="txt" name="txt" size="32"/>    <input type="button" id='btn01' name='btn01' value='BUTTON01'/>    <input type="button" id='btn02' name='btn02' value='BUTTON02'"/>    <input type="button" id='btn03' name='btn03' value='BUTTON03'/> </form> </body> </html> <script type="text/javascript"> var outerdata = '00'; $(document).ready(function(){ $('#txt').val('000000'); $('#btn01').bind('click',function(){ $.ajax({ type: "POST", url: "<%=request.getContextPath() %>/vumssmer/vmerservice!studyCallBack.do", data:'', success:function(msg){ console.log('msg.vflag:' + msg.vflag); console.log('msg.vmsg:' + msg.vmsg); $('#txt').val(msg.vflag); outerdata = msg.vflag; } }); if (outerdata == 'true'){ console.log('文本框內容是:' + $('#txt').val()); console.log('公共變量的值是:' + outerdata); console.log('yes'); }else{ console.log('文本框內容是:' + $('#txt').val()); console.log('公共變量的值是:' + outerdata); console.log('no'); } }); $('#btn02').bind('click',function(){ $.ajax({ type: "POST", url: "<%=request.getContextPath() %>/vumssmer/vmerservice!studyCallBack.do", data:'', success:function(msg){ console.log('msg.vflag:' + msg.vflag); console.log('msg.vmsg:' + msg.vmsg); $('#txt').val(msg.vflag); outerdata = msg.vflag; } }); }); $('#btn03').bind('click',function(){ if (outerdata == 'true'){ console.log('文本框內容是:' + $('#txt').val()); console.log('公共變量的值是:' + outerdata); console.log('yes'); }else{ console.log('文本框內容是:' + $('#txt').val()); console.log('公共變量的值是:' + outerdata); console.log('no'); } }); }); </script>
頁面的效果是,如圖2-1:

圖2-1
我們先點擊BUTTON2按鈕,再點擊BUTTON3按鈕,結果如下,如圖2-2:

圖2-2
這時的結果是正確的。
這到底是怎么回事了???
我們仔細看看兩次代碼的區別了,btn01的代碼都在一個函數里,而btn02和btn03的代碼分屬在不同的function里,我們再看看圖1-1里顯示的結果,打印出來的結果並沒有按照代碼的順序,if里的打印代碼先打印,而ajax的success方法里的代碼后打印的。這說明如果代碼在一個function里,if代碼會先於success里的代碼被執行,代碼並不是按我們書寫代碼的順序執行的。
對ajax熟悉的人都應該知道,我們處理ajax請求回來的結果都要定義一個回調函數,那么產生上面現象是不是因為回調函數都會在包含它的函數里滯后執行了。為了解開這個疑問,我做了如下的測試,大家看下面的callback.jsp代碼:
 
          
         View Code  
         <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>回調函數 CallBack Function Study</title> </head> <body> <form> <label for="txt"> 文本框: </label> <input type="text" id="txt" name="txt" size="32"/>   <input type="button" id='btn' name='btn' value='BUTTON' onclick="btnclick()"/> </form> </body> </html> <script type="text/javascript"> var $ = function(){ return document.getElementById(arguments[0]); } window.onload = function(){ $('txt').value = '11111'; } var staticnum = '000'; function btnclick(){ usedftn('true',callback); } function usedftn(flag,cbftn){ cbftn(flag); if (staticnum == 'true'){ console.log('公共變量的值是:' + staticnum); console.log('文本框內容是:' + $('txt').value); console.log('yes'); }else{ console.log('公共變量的值是:' + staticnum); console.log('文本框內容是:' + $('txt').value); console.log('no'); } } function callback(){ if (arguments[0] != null && arguments[0] != ''){ staticnum = arguments[0]; $('txt').value = arguments[0]; console.log('回調函數公共變量的值是:' + staticnum); console.log('回調函數文本框內容是:' + $('txt').value); } } </script>
執行的結果如下:

圖2-3
執行的結果是函數是按代碼順序執行。這和jQuery的ajax執行結果不同,那是不是因為jQuery代碼的寫法所導致的呢? jQuery代碼是通過匿名函數設計的,里面的jQuery對象是按照json的格式定義的,如是我把代碼更改成這樣的,代碼如下:
 
          
         View Code  
         <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>回調函數 CallBack Function Study</title> </head> <body> <form> <label for="txt"> 文本框: </label> <input type="text" id="txt" name="txt" size="32"/>   <input type="button" id='btn' name='btn' value='BUTTON'/> </form> </body> </html> <script type="text/javascript"> window.onload = function(){ document.getElementById('txt').value = '11111'; } var staticnum = '000'; (function(window,undefined){ var document = window.document,navigator = window.navigator,location = window.location, $ = function(){ return document.getElementById(arguments[0]); }; var xQuery = { xnum:'111', usedftn:function(flag,cbftn){ cbftn(flag); if (staticnum == 'true'){ console.log('公共變量的值是:' + staticnum); console.log('文本框內容是:' + $('txt').value); console.log('xQuery內部的xnum值是:' + xQuery.xnum); console.log('yes'); }else{ console.log('公共變量的值是:' + staticnum); console.log('文本框內容是:' + $('txt').value); console.log('xQuery內部的xnum值是:' + xQuery.xnum); console.log('no'); } if (xQuery.xnum == 'true'){ console.log('xQuery 公共變量的值是:' + staticnum); console.log('xQuery 文本框內容是:' + $('txt').value); console.log('xQuery xQuery內部的xnum值是:' + xQuery.xnum); console.log('xQuery yes'); }else{ console.log('xQuery 公共變量的值是:' + staticnum); console.log('xQuery 文本框內容是:' + $('txt').value); console.log('xQuery xQuery內部的xnum值是:' + xQuery.xnum); console.log('xQuery no'); } }, callback:function(){ if (arguments[0] != null && arguments[0] != ''){ staticnum = arguments[0]; $('txt').value = arguments[0]; xQuery.xnum = arguments[0]; console.log('回調函數公共變量的值是:' + staticnum); console.log('回調函數文本框內容是:' + $('txt').value); } }, xAttachBtnEvt:function(){ if (arguments[0] != null && arguments[0] != ''){ $(arguments[0]).onclick = function(){ //this.usedftn('true',this.callback);改代碼會出錯,因為綁定按鈕事件后this的指向變為了window了,而不是xQuery xQuery.usedftn('true',xQuery.callback); }; } } }; window.xQuery = window.$$ = xQuery; })(window); $$.xAttachBtnEvt('btn');//為按鈕綁定click事件 </script>
執行結果如下,如圖2-4:

圖2-4
結果是按代碼書寫順序執行的,看來不是javascript回調函數引起的上面的問題。
如果不是回調函數那么就應該是ajax本身了。
這里我還是按照jQuery的結構來寫實例代碼,ajax使用原生態的方式編寫,這樣會讓我們探討的問題更加清晰,代碼如下:
 
          
         View Code  
         <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>回調函數 CallBack Function Study</title> </head> <body> <form> <label for="txt"> 文本框: </label> <input type="text" id="txt" name="txt" size="32"/>   <input type="button" id='btn' name='btn' value='BUTTON'/> </form> </body> </html> <script type="text/javascript"> window.onload = function(){ document.getElementById('txt').value = '11111'; } var staticnum = '000'; (function(window,undefined){ var document = window.document,navigator = window.navigator,location = window.location, $ = function(){ return document.getElementById(arguments[0]); }; var xQuery = { xnum:'1111', type:'GET', url:'wwww.baidu.com', xmlHttp:'', createXMLHttpRequest:function(){ if (window.XMLHttpRequest){ // IE7+, Firefox, Chrome, Opera, Safari this.xmlHttp = new XMLHttpRequest(); }else{ // IE6, IE5 this.xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); } }, ajaxftn:function(ajaxdata){ this.type = ajaxdata.type; this.url = ajaxdata.url; this.createXMLHttpRequest(); this.xmlHttp.open(this.type,this.url,true); this.xmlHttp.onreadystatechange = this.jsonCallBack; this.xmlHttp.setRequestHeader("Content-Type","application/x-www-form-urlencoded;");//使用POST傳遞信息時候用到的 this.xmlHttp.send(null); if (staticnum == 'true'){ console.log('公共變量的值是:' + staticnum); console.log('文本框內容是:' + $('txt').value); console.log('xQuery內部的xnum值是:' + this.xnum); console.log('yes'); }else{ console.log('公共變量的值是:' + staticnum); console.log('文本框內容是:' + $('txt').value); console.log('xQuery內部的xnum值是:' + this.xnum); console.log('no'); } if (this.xnum == 'true'){ console.log('xQuery 公共變量的值是:' + staticnum); console.log('xQuery 文本框內容是:' + $('txt').value); console.log('xQuery xQuery內部的xnum值是:' + this.xnum); console.log('xQuery yes'); }else{ console.log('xQuery 公共變量的值是:' + staticnum); console.log('xQuery 文本框內容是:' + $('txt').value); console.log('xQuery xQuery內部的xnum值是:' + this.xnum); console.log('xQuery no'); } }, jsonCallBack:function(){ if (xQuery.xmlHttp.readyState == 4){ if (xQuery.xmlHttp.status == 200){ xQuery.parseResults(); } } }, parseResults:function(){ var retval = eval('('+ xQuery.xmlHttp.responseText +')'); console.log('服務端返回的vflag值:' + retval.vflag); console.log('服務端返回的vmsg值:' + retval.vmsg); $('txt').value = retval.vflag; staticnum = retval.vflag; xQuery.xnum = retval.vflag; }, xAttachBtnEvt:function(){ if (arguments[0] != null && arguments[0] != ''){ $(arguments[0]).onclick = function(){ xQuery.ajaxftn({'type':'POST','url':'<%=request.getContextPath() %>/vumssmer/vmerservice!studyCallBack.do'}); }; } } }; window.xQuery = window.$$ = xQuery; })(window); $$.xAttachBtnEvt('btn');//為按鈕綁定click事件 </script>
結果如圖2-5所示:

圖2-6
這個結果就和我們調用jQuery的ajax方法的結果一樣了。
我的研究過程就是這樣了,下面就是我的分析結果了。
1.3我的分析結果
首先我要講講javascript里回調函數到底是怎么回事。回調函數在編程語言里很普遍,java里面也有,百度百科里有對回調函數的定義:
回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用為調用它所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應。
詳情可以參見:
http://baike.baidu.com/view/414773.htm
我們這里不講回調函數實際運用的場景,從編碼角度,回調函數和調用回調函數的函數是一個統一的整體,他們在執行上是按照代碼編寫的順序至上而下的。
我在學習ajax時候,我看的書籍上都寫到onreadystatechange要賦一個回調函數,那么按照上面的結論我們在“開發問題中”寫的代碼應該能正常運行,但是結果卻恰恰相反。
難道ajax的onreadystatechange存儲的不是我們通常理解的回調函數嗎?或者是ajax有自己特別的回調機制嗎?
我的回答是onreadystatechange存儲的是回調函數也沒有什么特別的回調機制,但它不是被執行在我們所寫的調用ajax方法內的回調函數,而是瀏覽器執行XMLHttpRequest請求里面的回調函數,我們書寫的我們寫的:
this.xmlHttp.onreadystatechange = this.jsonCallBack;
只是在為onreadystatechange做賦值操作。因此我們在執行我們自己編寫的ajax函數時候onreadystatechange存儲的函數是不會被調用的,因為這只是一個賦值操作。
那什么時候執行onreadystatechange存儲的回調函數呢?當我們的ajax請求被成功的返回值以后,調用到了onreadystatechange存儲的回調函數,回調函數就被執行了,這就是我們看到success函數里的代碼會滯后於我們編寫的ajax調用方法的原因所在。
在我寫的代碼里,ajax里的onreadystatechange存儲回調函數我都是用xQuery.xnum、xQuery.xmlHttp調用xQuery里的方法,而不是this,大家可以試試把代碼改成用this調用,最后firebug結果會表現為this.xmlHttp沒有定義之類的提示,這個也反向說明了回調函數調用的時候已經脫離了原來方法而變成了一個獨立的方法,因此我們存儲的回調函數所使用的變量一定要在一個公共作用域里,因此使用了xQuery來存儲變量。
以上的結論我們可以糾正對ajax調用幾個錯誤的理解:
1. ajax里面的回調函數的調用機制是一種特別的機制,它與javascript普通回調函數的使用不一樣;
2. ajax回調函數的作用域和ajax調用函數作用域的不同而引起的代碼不能正常運行。
正確的理解應該是:
Ajax里的回調函數只是我們賦值給XMLHttpRequest對象的回調函數,它的執行和我們所寫的調用ajax函數無關。
實際運用中如果我們想在執行完ajax請求后,根據請求結果執行相關的邏輯,那么請把邏輯寫在ajax的回調函數里,只有這樣才能讓代碼按業務邏輯正常運行。
