整體結構html
dojo/request/script、dojo/request/xhr、dojo/request/iframe這三者是dojo提供的provider。dojo將內部的全部provider構建在Deferred基礎上造成異步鏈式模型,utils.deferred函數向3個provider提供統一接口來規範其行爲。數據請求在各個provider的發送過程幾乎一致:node
//Make the Deferred object for this xhr request. var dfd = util.deferred( response, cancel, isValid, isReady, handleResponse, last );
parseArgs函數主要處理三個參數:data(POST方法有效)、query(GET方法有效)、preventCache(添加時間戳防止緩存)ajax
1 exports.parseArgs = function parseArgs(url, options, skipData){ 2 var data = options.data, 3 query = options.query; 4 5 if(data && !skipData){ 6 if(typeof data === 'object'){ 7 options.data = ioQuery.objectToQuery(data); 8 } 9 } 10 11 if(query){ 12 if(typeof query === 'object'){ 13 query = ioQuery.objectToQuery(query); 14 } 15 if(options.preventCache){ 16 query += (query ? '&' : '') + 'request.preventCache=' + (+(new Date)); 17 } 18 }else if(options.preventCache){ 19 query = 'request.preventCache=' + (+(new Date)); 20 } 21 22 if(url && query){ 23 url += (~url.indexOf('?') ? '&' : '?') + query; 24 } 25 26 return { 27 url: url, 28 options: options, 29 getHeader: function(headerName){ return null; } 30 }; 31 };
返回的response,是一個表明服務器端返回結果的對象,在這裏它還只是一個半成品,須要handleResponse函數中爲其裝填數據。json
utils.deferred使用爲各provider提供統一的接口,來規範數據處理流程,在各provider中須要提供如下參數:跨域
utils.deferred函數中作了如下三件事:數組
1 exports.deferred = function deferred(response, cancel, isValid, isReady, handleResponse, last){ 2 var def = new Deferred(function(reason){ 3 cancel && cancel(def, response); 4 5 if(!reason || !(reason instanceof RequestError) && !(reason instanceof CancelError)){ 6 return new CancelError('Request canceled', response); 7 } 8 return reason; 9 }); 10 11 def.response = response; 12 def.isValid = isValid; 13 def.isReady = isReady; 14 def.handleResponse = handleResponse; 15 16 function errHandler(error){ 17 error.response = response; 18 throw error; 19 } 20 var responsePromise = def.then(okHandler).otherwise(errHandler); 21 22 if(exports.notify){ 23 responsePromise.then( 24 lang.hitch(exports.notify, 'emit', 'load'), 25 lang.hitch(exports.notify, 'emit', 'error') 26 ); 27 } 28 29 var dataPromise = responsePromise.then(dataHandler); 30 31 // http://bugs.dojotoolkit.org/ticket/16794 32 // The following works around a leak in IE9 through the 33 // prototype using lang.delegate on dataPromise and 34 // assigning the result a property with a reference to 35 // responsePromise. 36 var promise = new Promise(); 37 for (var prop in dataPromise) { 38 if (dataPromise.hasOwnProperty(prop)) { 39 promise[prop] = dataPromise[prop]; 40 } 41 } 42 promise.response = responsePromise; 43 freeze(promise); 44 // End leak fix 45 46 47 if(last){ 48 def.then(function(response){ 49 last.call(def, response); 50 }, function(error){ 51 last.call(def, response, error); 52 }); 53 } 54 55 def.promise = promise; 56 def.then = promise.then;//利用閉包(waiting數組在deferred模塊中是一個全局變量,) 57 58 return def; 59 };
請求成功後整個數據處理流程以下:promise
watch模塊經過不斷tick方式來監控請求隊列,離開隊列的方式有四種:瀏覽器
1 var _inFlightIntvl = null, 2 _inFlight = []; 3 4 function watchInFlight(){ 5 // summary: 6 // internal method that checks each inflight XMLHttpRequest to see 7 // if it has completed or if the timeout situation applies. 8 9 var now = +(new Date); 10 // we need manual loop because we often modify _inFlight (and therefore 'i') while iterating 11 for(var i = 0, dfd; i < _inFlight.length && (dfd = _inFlight[i]); i++){ 12 var response = dfd.response, 13 options = response.options; 14 if((dfd.isCanceled && dfd.isCanceled()) || (dfd.isValid && !dfd.isValid(response))){ 15 _inFlight.splice(i--, 1); 16 watch._onAction && watch._onAction(); 17 }else if(dfd.isReady && dfd.isReady(response)){ 18 _inFlight.splice(i--, 1); 19 dfd.handleResponse(response); 20 watch._onAction && watch._onAction(); 21 }else if(dfd.startTime){ 22 // did we timeout? 23 if(dfd.startTime + (options.timeout || 0) < now){ 24 _inFlight.splice(i--, 1); 25 // Cancel the request so the io module can do appropriate cleanup. 26 dfd.cancel(new RequestTimeoutError('Timeout exceeded', response)); 27 watch._onAction && watch._onAction(); 28 } 29 } 30 } 31 watch._onInFlight && watch._onInFlight(dfd); 32 33 if(!_inFlight.length){ 34 clearInterval(_inFlightIntvl); 35 _inFlightIntvl = null; 36 } 37 } 38 39 function watch(dfd){ 40 // summary: 41 // Watches the io request represented by dfd to see if it completes. 42 // dfd: Deferred 43 // The Deferred object to watch. 44 // response: Object 45 // The object used as the value of the request promise. 46 // validCheck: Function 47 // Function used to check if the IO request is still valid. Gets the dfd 48 // object as its only argument. 49 // ioCheck: Function 50 // Function used to check if basic IO call worked. Gets the dfd 51 // object as its only argument. 52 // resHandle: Function 53 // Function used to process response. Gets the dfd 54 // object as its only argument. 55 if(dfd.response.options.timeout){ 56 dfd.startTime = +(new Date); 57 } 58 59 if(dfd.isFulfilled()){ 60 // bail out if the deferred is already fulfilled 61 return; 62 } 63 64 _inFlight.push(dfd); 65 if(!_inFlightIntvl){ 66 _inFlightIntvl = setInterval(watchInFlight, 50); 67 } 68 69 // handle sync requests separately from async: 70 // http://bugs.dojotoolkit.org/ticket/8467 71 if(dfd.response.options.sync){ 72 watchInFlight(); 73 } 74 } 75 76 watch.cancelAll = function cancelAll(){ 77 // summary: 78 // Cancels all pending IO requests, regardless of IO type 79 try{ 80 array.forEach(_inFlight, function(dfd){ 81 try{ 82 dfd.cancel(new CancelError('All requests canceled.')); 83 }catch(e){} 84 }); 85 }catch(e){} 86 }; 87 88 if(win && on && win.doc.attachEvent){ 89 // Automatically call cancel all io calls on unload in IE 90 // http://bugs.dojotoolkit.org/ticket/2357 91 on(win.global, 'unload', function(){ 92 watch.cancelAll(); 93 }); 94 }
dojo/request/script緩存
經過script模塊經過動態添加script標籤的方式發送請求,該模塊支持兩種方式來獲取數據服務器
無論使用哪一種方式都是以get方式來大宋數據,同時後臺必須返回原生的js對象,因此不須要設置handleAs參數。如下是script處理、發送請求的源碼:
1 function script(url, options, returnDeferred){ 2 //解析參數,生成半成品response 3 var response = util.parseArgs(url, util.deepCopy({}, options)); 4 url = response.url; 5 options = response.options; 6 7 var dfd = util.deferred(//構建dfd對象 8 response, 9 canceler, 10 isValid, 11 //這裏分爲三種狀況:jsonp方式無需isReady函數; 12 //checkString方式須要不斷檢查checkString制定的全局變量; 13 //js腳本方式須要檢查script標籤是否進入load事件 14 options.jsonp ? null : (options.checkString ? isReadyCheckString : isReadyScript), 15 handleResponse 16 ); 17 18 lang.mixin(dfd, { 19 id: mid + (counter++), 20 canDelete: false 21 }); 22 23 if(options.jsonp){//處理callback參數,注意加?仍是&;有代理狀況尤其注意,proxy?url這種狀況的處理 24 var queryParameter = new RegExp('[?&]' + options.jsonp + '='); 25 if(!queryParameter.test(url)){ 26 url += (~url.indexOf('?') ? '&' : '?') + 27 options.jsonp + '=' + 28 (options.frameDoc ? 'parent.' : '') + 29 mid + '_callbacks.' + dfd.id; 30 } 31 32 dfd.canDelete = true; 33 callbacks[dfd.id] = function(json){ 34 response.data = json; 35 dfd.handleResponse(response); 36 }; 37 } 38 39 if(util.notify){//ajax全局事件 40 util.notify.emit('send', response, dfd.promise.cancel); 41 } 42 43 if(!options.canAttach || options.canAttach(dfd)){ 44 //建立script元素髮送請求 45 var node = script._attach(dfd.id, url, options.frameDoc); 46 47 if(!options.jsonp && !options.checkString){ 48 //script加載完畢後設置scriptLoaded,isReadyScript中使用 49 var handle = on(node, loadEvent, function(evt){ 50 if(evt.type === 'load' || readyRegExp.test(node.readyState)){ 51 handle.remove(); 52 dfd.scriptLoaded = evt; 53 } 54 }); 55 } 56 } 57 //watch監控請求隊列,抹平timeout處理,只有ie跟xhr2才支持原生timeout屬性;def.isValid表示是否在檢查範圍內; 58 watch(dfd); 59 60 return returnDeferred ? dfd : dfd.promise; 61 }
獲得數據後,script模塊會刪除剛剛添加的script元素。按照咱們上面分析的處理邏輯,last函數用於在請求結束後作其餘邏輯處理,因此我認爲正確的邏輯是放在last中刪除script元素,可是dojo中爲了兼容低版本ie瀏覽器,將刪除工做放在了isValid函數中。
1 function isValid(response){ 2 //Do script cleanup here. We wait for one inflight pass 3 //to make sure we don't get any weird things by trying to remove a script 4 //tag that is part of the call chain (IE 6 has been known to 5 //crash in that case). 6 if(deadScripts && deadScripts.length){ 7 array.forEach(deadScripts, function(_script){ 8 script._remove(_script.id, _script.frameDoc); 9 _script.frameDoc = null; 10 }); 11 deadScripts = []; 12 } 13 14 return response.options.jsonp ? !response.data : true; 15 }
發送處理請求的整個過程以下:
dojo/request/xhr
整個xhr.js分爲如下幾個部分:
xhr函數的處理過程以下:
1 function xhr(url, options, returnDeferred){ 2 //解析參數 3 var isFormData = has('native-formdata') && options && options.data && options.data instanceof FormData; 4 var response = util.parseArgs( 5 url, 6 util.deepCreate(defaultOptions, options), 7 isFormData 8 ); 9 url = response.url; 10 options = response.options; 11 12 var remover, 13 last = function(){ 14 remover && remover();//對於xhr2,在請求結束後移除綁定事件 15 }; 16 17 //Make the Deferred object for this xhr request. 18 var dfd = util.deferred( 19 response, 20 cancel, 21 isValid, 22 isReady, 23 handleResponse, 24 last 25 ); 26 var _xhr = response.xhr = xhr._create();//建立請求對象 27 28 if(!_xhr){ 29 // If XHR factory somehow returns nothings, 30 // cancel the deferred. 31 dfd.cancel(new RequestError('XHR was not created')); 32 return returnDeferred ? dfd : dfd.promise; 33 } 34 35 response.getHeader = getHeader; 36 37 if(addListeners){//若是是xhr2,綁定xhr的load、progress、error事件 38 remover = addListeners(_xhr, dfd, response); 39 } 40 41 var data = options.data, 42 async = !options.sync, 43 method = options.method; 44 45 try{//發送請求以前處理其餘參數:responseType、withCredential、headers 46 // IE6 won't let you call apply() on the native function. 47 _xhr.open(method, url, async, options.user || undefined, options.password || undefined); 48 if(options.withCredentials){ 49 _xhr.withCredentials = options.withCredentials; 50 } 51 if(has('native-response-type') && options.handleAs in nativeResponseTypes) { 52 _xhr.responseType = nativeResponseTypes[options.handleAs]; 53 } 54 var headers = options.headers, 55 contentType = isFormData ? false : 'application/x-www-form-urlencoded'; 56 if(headers){//對於X-Requested-With單獨處理 57 for(var hdr in headers){ 58 if(hdr.toLowerCase() === 'content-type'){ 59 contentType = headers[hdr]; 60 }else if(headers[hdr]){ 61 //Only add header if it has a value. This allows for instance, skipping 62 //insertion of X-Requested-With by specifying empty value. 63 _xhr.setRequestHeader(hdr, headers[hdr]); 64 } 65 } 66 } 67 if(contentType && contentType !== false){ 68 _xhr.setRequestHeader('Content-Type', contentType); 69 } 70 //瀏覽器根據這個請求頭來判斷http請求是否由ajax方式發出, 71 //設置X-Requested-with:null以欺騙瀏覽器的方式進行跨域請求(不多使用) 72 if(!headers || !('X-Requested-With' in headers)){ 73 _xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 74 } 75 if(util.notify){ 76 util.notify.emit('send', response, dfd.promise.cancel); 77 } 78 _xhr.send(data); 79 }catch(e){ 80 dfd.reject(e); 81 } 82 83 watch(dfd); 84 _xhr = null; 85 86 return returnDeferred ? dfd : dfd.promise; 87 }
X-Requested-With請求頭用於在服務器端判斷request來自Ajax請求仍是傳統請求(判不判斷是服務器端的事情)。傳統同步請求沒有這個header頭,而ajax請求瀏覽器會加上這個頭,能夠經過xhr.setRequestHeader('X-Requested-With', null)來避免瀏覽器進行preflight請求。
xhr模塊的整個請求流程以下:
dojo/request/iframe
用於xhr沒法完成的複雜的請求/響應,體現於兩方面:
若是返回的數據不是html或xml格式,好比text、json,必須將數據放在textarea標籤中,這是惟一一種能夠兼容各個瀏覽器的獲取返回數據的方式。
至於爲何要放到textarea標籤中,textarea適合大塊文本的輸入,textbox只適合單行內容輸入,而若是直接將數據以文本形式放到html頁面中,某些特殊字符會被轉義。注意後臺返回的content-type必須是text/html。
關於iframe上傳文件的原理請看個人這篇博客:Javascript無刷新上傳文件
使用iframe發送的全部請求都會被裝填到一個隊列中,這些請求並非並行發送而是依次發送,由於該模塊只會建立一個iframe。理解了這一點是看懂整個iframe模塊代碼的關鍵。
iframe函數的源碼,與上兩個provider相似
1 function iframe(url, options, returnDeferred){ 2 var response = util.parseArgs(url, util.deepCreate(defaultOptions, options), true); 3 url = response.url; 4 options = response.options; 5 6 if(options.method !== 'GET' && options.method !== 'POST'){ 7 throw new Error(options.method + ' not supported by dojo/request/iframe'); 8 } 9 10 if(!iframe._frame){ 11 iframe._frame = iframe.create(iframe._iframeName, onload + '();'); 12 } 13 14 var dfd = util.deferred(response, null, isValid, isReady, handleResponse, last); 15 16 //_callNext有last函數控制,其中調用_fireNextRequest構成了整個dfdQueue隊列調用 17 dfd._callNext = function(){ 18 if(!this._calledNext){ 19 this._calledNext = true; 20 iframe._currentDfd = null; 21 iframe._fireNextRequest(); 22 } 23 }; 24 dfd._legacy = returnDeferred; 25 26 iframe._dfdQueue.push(dfd); 27 iframe._fireNextRequest(); 28 29 watch(dfd); 30 31 return returnDeferred ? dfd : dfd.promise; 32 }
主要看一下iframe模塊的請求、處理流程:
dojo的源碼中有大部分處理兼容性的內容,在本篇博客中並未作詳細探討。看源碼主要看總體的處理流程和設計思想,兼容性靠的是基礎的積累。同時經過翻看dojo源碼我也發現本身的薄弱環節,對於dojo源碼的解析暫時告一段落,回去惡補基礎。。。