dojo Provider(script、xhr、iframe)源碼解析

  整體結構html

  dojo/request/script、dojo/request/xhr、dojo/request/iframe這三者是dojo提供的provider。dojo將內部的全部provider構建在Deferred基礎上造成異步鏈式模型,utils.deferred函數向3個provider提供統一接口來規範其行爲。數據請求在各個provider的發送過程幾乎一致:node

  1. 解析options參數util.parseArgs
  2. 建立dfd對象,該對象控制着整個數據接收、處理、傳遞的過程
    //Make the Deferred object for this xhr request.
            var dfd = util.deferred(
                response,
                cancel,
                isValid,
                isReady,
                handleResponse,
                last
            );
  3. 建立處理last函數(script沒有該過程)
  4. 發送請求
  5. watch

  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     };
View Code

  返回的response,是一個表明服務器端返回結果的對象,在這裏它還只是一個半成品,須要handleResponse函數中爲其裝填數據。json

  utils.deferred使用爲各provider提供統一的接口,來規範數據處理流程,在各provider中須要提供如下參數:跨域

  • 上文中生成的response對象
  • cancel:數據請求被取消以後,provider作本身的邏輯處理
  • isValid根據某些屬性判斷是否要繼續留在_inFlight隊列裏面(是否還須要進行timeout檢查),一般調用handleResponse結束後,isValid爲false
  • isReady:根據某些屬性判斷請求是否成功,成功後調用handleResponse
  • handleResponse:對數據傳輸的成功與否作不一樣邏輯處理,由兩種方式觸發:provider內部根據某些事件觸發(如XMLHttpRequest的load事件),watch模塊中不斷tick檢查,isReady爲true時觸發;請求成功後provider有本身的邏輯處理,經過handlers數據轉換器爲response裝填data和text(有的話),有的provider不須要handlers好比script
  • last做爲dfd的第二波鏈式回調處理,主要做用是在本次請求結束以後的其餘邏輯處理

  utils.deferred函數中作了如下三件事:數組

  1. 建立deferred對象
  2. 爲dfd對象裝填isValid、isReady、handleResponse方法
  3. 規範數據處理流程
 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     };
View Code

  請求成功後整個數據處理流程以下:promise

  watch模塊經過不斷tick方式來監控請求隊列,離開隊列的方式有四種:瀏覽器

  1. provider本身觸發handleResponse後dfd.isValid爲false,移出監控隊列
  2. dfd.isReady爲true後觸發handleResponse,移出監控隊列
  3. timeout超時,調用dfd.cancel取消請求,移出隊列
  4. window unload事件中取消全部請求,清空隊列
 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     }
View Code

 

  dojo/request/script緩存

  經過script模塊經過動態添加script標籤的方式發送請求,該模塊支持兩種方式來獲取數據服務器

  • 設置jsonp參數,以jsonp形式來獲取服務器端數據
  • 設置checkString參數,將後臺返回的數據掛載到一個全局對象中,經過不斷的tick方式檢查全局對象是否賦值來進入fulfill回調
  • 若是兩個參數都沒設置,該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     }
View Code

  獲得數據後,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分爲如下幾個部分:

  1. 特性檢測
  2. handleResponse函數
  3. 對於不一樣的XMLHttpRequest使用不一樣的isValid、isReady、cancel函數
  4. 建立xhr provider
  5. 根據不一樣條件使用不一樣的create函數

  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     }
View Code

  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     }
View Code

 

  主要看一下iframe模塊的請求、處理流程:

  

  

  dojo的源碼中有大部分處理兼容性的內容,在本篇博客中並未作詳細探討。看源碼主要看總體的處理流程和設計思想,兼容性靠的是基礎的積累。同時經過翻看dojo源碼我也發現本身的薄弱環節,對於dojo源碼的解析暫時告一段落,回去惡補基礎。。。

相關文章
相關標籤/搜索