• dojo Provider(script、xhr、iframe)源码解析


      总体结构

      dojo/request/script、dojo/request/xhr、dojo/request/iframe这三者是dojo提供的provider。dojo将内部的所有provider构建在Deferred基础上形成异步链式模型,utils.deferred函数向3个provider提供统一接口来规范其行为。数据请求在各个provider的发送过程几乎一致:

    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(添加时间戳防止缓存)

     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函数中为其装填数据。

      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

      请求成功后整个数据处理流程如下:

      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源码的解析暂时告一段落,回去恶补基础。。。

  • 相关阅读:
    Python小白的数学建模 ---- 系列课程
    Maven学习笔记
    JavaScript 中的 Var,Let 和 Const 有什么区别
    (鸡汤文)搞懂了 JavaScript 定时器 setTimeout() 的 this 指向!
    setTimeout返回值的验证,(〒︿〒) 请原谅我一直以来对你的忽视
    终于把初中到大学的数学知识梳理完了(学习算法必备数学知识)
    最简单入门深度学习
    机器学习基本流程
    Vue.js源码解析-Vue初始化流程
    最大公约数&最小公倍数
  • 原文地址:https://www.cnblogs.com/dojo-lzz/p/4456504.html
Copyright © 2020-2023  润新知