jQuery源码分析之ajaxTransport和ajaxPrefilters执行函数之inspectPrefiltersOrTransports

源码分析:(用于真正处理执行ajaxPrefilters和ajaxTransport的逻辑代码,参考

function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
	var inspected = {},
		seekingTransport = ( structure === transports );
	function inspect( dataType ) {
		var selected;
		inspected[ dataType ] = true;//这种数据类型已经检查过了
		jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
			var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
			if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
				options.dataTypes.unshift( dataTypeOrTransport );
				inspect( dataTypeOrTransport );
				return false;
			} else if ( seekingTransport ) {
				return !( selected = dataTypeOrTransport );
			}
		});
		return selected;//返回的select有send,abort等方法!
	}
	return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
}

注意:

(1)上面用的是jQuery.each方法,如果有return false那么直接跳出循环。

(2)同时我们一开始是检测options.dataTypes[0],也就是执行我们传入的dataTypes[0]的回调函数集合,如果我们明确传入了dataType那么这里就不会是"*"否则默认是"*"!这种"*"表示通过ajaxTransport和ajaxPrefilters添加函数的时候没有明确指定数据类型,那么数据类型就被设置为"*"!表示任何数据类型都会进行过滤

(3)总之,如果我们明确指定了dataType那么我们就检测dataType[0],如果检测dataType[0]时候返回的值转换为false那么我们还会继续执行所有的"*"也就是通用过滤器!最终返回一个对象,这个对象就是select就是执行通过ajaxTransport和ajaxPrefilters添加的函数执行的返回值,该对象有send等方法,也是ajax请求真正起作用的地方!获取这个对象就可以发送请求了!

note:inspectPrefiltersOrTransports方法在ajax方法中被执行了两次,第一次执行所有的ajaxPrefilters第二次执行所有的ajaxTransport!但是这里面很显然有一次执行的时候判断返回值是否是string,如果是string同时调用结果也是在ajaxprefilters中,而不是ajaxTransport中,而且这种数据类型还没有被检查过!那么把这种类型放入到dataTypes中,而且放在dataTypes的最前面,然后继续对这种类型检查!那么这个判断有什么用呢?看下面代码:

jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
	var callbackName, overwritten, responseContainer,
		jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
			"url" :
typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
		);
	// Handle iff the expected data type is "jsonp" or we have a parameter to set
	if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
		// Get callback name, remembering preexisting value associated with it
		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
			s.jsonpCallback() :
			s.jsonpCallback;
		// Insert callback into url or form data
		if ( jsonProp ) {
			s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
		} else if ( s.jsonp !== false ) {
			s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
		}
		// Use data converter to retrieve json after script execution
		s.converters["script json"] = function() {
			if ( !responseContainer ) {
				jQuery.error( callbackName + " was not called" );
			}
			return responseContainer[ 0 ];
		};
		// force json dataType
		s.dataTypes[ 0 ] = "json";//把dataTypes[0]设置为json,也就是返回值是json!
		// Install callback
		overwritten = window[ callbackName ];
		window[ callbackName ] = function() {
			responseContainer = arguments;
		};
		// Clean-up function (fires after converters)
		jqXHR.always(function() {
			// Restore preexisting value
			window[ callbackName ] = overwritten;
			// Save back as free
			if ( s[ callbackName ] ) {
				// make sure that re-using the options doesn't screw things around
				s.jsonpCallback = originalSettings.jsonpCallback;
				// save the callback name for future use
				oldCallbacks.push( callbackName );
			}
			// Call if it was a function and we have a response
			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
				overwritten( responseContainer[ 0 ] );
			}
			responseContainer = overwritten = undefined;
		});
		// Delegate to script
		return "script";
	}
});

note:对于json或者jsonp这种数据的预处理发生了URL的重构,同时最后返回了“script”用于inspectPrefiltersOrTransports函数。所以在用户的dataType设置为"json"或者"jsonp"的时候我们会进行URL重构,同时重构结束以后会在函数inspectPrefiltersOrTransports里继续对script标签进行预先处理,也就说当对json或者jsonp处理完毕以后就会处理transport或者prefilters里面的script集合中的函数,要记住在prefilters或者transport里面以script为键名放置的其实是一个数组

我们现在看看jQuery通过ajaxTransport方法添加的一个通用的回调函数:

	jQuery.ajaxTransport(function( options ) {
		// Cross domain only allowed if supported through XMLHttpRequest
		if ( !options.crossDomain || support.cors ) {
			var callback;
			return {
				send: function( headers, complete ) {
					var i,
						xhr = options.xhr(),
						id = ++xhrId;
					// Open the socket
					xhr.open( options.type, options.url, options.async, options.username, options.password );
					// Apply custom fields if provided
					if ( options.xhrFields ) {
						for ( i in options.xhrFields ) {
							xhr[ i ] = options.xhrFields[ i ];
						}
					}
					// Override mime type if needed
					if ( options.mimeType && xhr.overrideMimeType ) {
						xhr.overrideMimeType( options.mimeType );
					}
					// X-Requested-With header
					// For cross-domain requests, seeing as conditions for a preflight are
					// akin to a jigsaw puzzle, we simply never set it to be sure.
					// (it can always be set on a per-request basis or even using ajaxSetup)
					// For same-domain requests, won't change header if already provided.
					if ( !options.crossDomain && !headers["X-Requested-With"] ) {
						headers["X-Requested-With"] = "XMLHttpRequest";
					}
					// Set headers
					for ( i in headers ) {
						// Support: IE<9
						// IE's ActiveXObject throws a 'Type Mismatch' exception when setting
						// request header to a null-value.
						//
						// To keep consistent with other XHR implementations, cast the value
						// to string and ignore `undefined`.
						if ( headers[ i ] !== undefined ) {
							xhr.setRequestHeader( i, headers[ i ] + "" );
						}
					}
					// Do send the request
					// This may raise an exception which is actually
					// handled in jQuery.ajax (so no try/catch here)
					xhr.send( ( options.hasContent && options.data ) || null );
					// Listener
					callback = function( _, isAbort ) {
						var status, statusText, responses;
						// Was never called and is aborted or complete
						if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
							// Clean up
							delete xhrCallbacks[ id ];
							callback = undefined;
							xhr.onreadystatechange = jQuery.noop;
                       //在abort方法里面调用callback(undefined,true)这里就会把回调函数什么都置空,如果完成了readyState=4也会置空回调函数!
							// Abort manually if needed
							if ( isAbort ) {//执行if如果调用了abort方法!
								if ( xhr.readyState !== 4 ) {
									xhr.abort();
								}
							} else {
								responses = {};
								status = xhr.status;
								// Support: IE<10
								// Accessing binary-data responseText throws an exception
								// (#11426)
								if ( typeof xhr.responseText === "string" ) {
									responses.text = xhr.responseText;
								}
								// Firefox throws an exception when accessing
								// statusText for faulty cross-domain requests
								try {
									statusText = xhr.statusText;
								} catch( e ) {
									// We normalize with Webkit giving an empty statusText
									statusText = "";
								}
								// Filter status for non standard behaviors
								// If the request is local and we have data: assume a success
								// (success with no data won't get notified, that's the best we
								// can do given current implementations)
								if ( !status && options.isLocal && !options.crossDomain ) {
									status = responses.text ? 200 : 404;
								// IE - #1450: sometimes returns 1223 when it should be 204
								} else if ( status === 1223 ) {
									status = 204;
								}
							}
						}
						// Call complete if needed
						if ( responses ) {//status是状态码,statusText就是状态信息,responses就是服务器返回内容
							complete( status, statusText, responses, xhr.getAllResponseHeaders() );
						}
					};
					if ( !options.async ) {
						// if we're in sync mode we fire the callback
						callback();
					} else if ( xhr.readyState === 4 ) {
						// (IE6 & IE7) if it's in cache and has been
						// retrieved directly we need to fire the callback
						setTimeout( callback );
					} else {
						// Add to the list of active xhr callbacks回调函数callback第一个参数是event对象!
						xhr.onreadystatechange = xhrCallbacks[ id ] = callback;
					}
				},
				abort: function() {
					if ( callback ) {
						callback( undefined, true );
					}
				}
			};
		}
	});
note:这个通用的回调函数的使用返回就是上面说过的"*",对于任何类型都会处理。当在inspectPrefiltersOrTransports函数中执行的时候就会返回select,是一个对象,该对象有send方法用于真正发送请求!函数执行的时候传入的参数是options,originalOptions,jqXHR对象!同时要调用send方法的时候,我们会传入两个参数,通过setRequestHeader方法传入的HTTP头和调用成功时候的回调函数。调用时候的过程为:

(1)通过open方法打开socket流,并且附加用户传入的xhrFields头部信息。xhrFields是一个具有多个"字段名称-字段值"对的对象,用于对本地XHR对象进行设置。一对「文件名-文件值」在本机设置XHR对象。例如,如果需要,你可以用它来为跨域请求设置XHR对象的withCredentials属性为true

(2)为XHR对象设置用户自己提供的mimeType类型,并且把X-Requested-With放入headers集合中,最终把用户提供的headers选项通过setRequestHeader赋值到XHR对象上headers默认值:{}以对象形式指定附加的请求头信息。请求头X-Requested-With: XMLHttpRequest将始终被添加,当然你也可以在此处修改默认的XMLHttpRequest值。headers中的值可以覆盖beforeSend回调函数中设置的请求头(意即beforeSend先被调用)。headers选项是通过调用xhr对象的setRequestHeader方法来完成的!

(3)设置完XHR的头部以后就调用send方法了,如果不是get/head请求那么把用户提供的数据一同发送出去,get/head方法已经把参数附加到URL后面!

我们通过阅读源码看出,调用transport.send( requestHeaders, done );方法的时候是在获取到transport方法以后,然后传入的回调函数是done方法(见下面源码)!

(4)如果async是false,表示不是异步,那么直接调用上面代码中的回调函数callback。如果用户指定了异步,同时readyState已经是4,表示已经完成了请求了。(4 - (完成)响应内容解析完成,可以在客户端调用了 )那么这时候也直接调用callback(因为在IE6、7中会缓存,我们要手动调用)。如果不是上面两种情况,例如"用户指定了async为true,同时readyState也不是4,那么我们直接把这个callback回调函数绑定到xhr的onreadystatechange事件中"。我们要注意上面代码中有xhrCallbacks[id],用处在那里呢?我们看看定义:var xhrId = 0,xhrCallbacks = {},

同时在ajaxTransport中每调用一次send方法id = ++xhrId;xhr.onreadystatechange = xhrCallbacks[ id ] = callback;这表示:每次调用send方法都会把这个函数放在xhrCallbacks对象中保存起来,其中键名是表示第几次调用send方法!键值就是回调函数!

(5)我们看看在回调函数callback中干了什么?第一步:如果请求已经完成或者已经取消那么我们xhrCallbacks中的相应的回调删除,同时把callback清空为undefined,已经onreadystatechange设置为空函数。如果isAbort为true,同时readyState不是4,表示没有完成,那么手动调用abort方法!如果isAbort不是true,那么表示已经完成了,这时候获取xhr对象的status,responseText,statusText,getAllResponseHeaders,同时把xhr对象的responseText封装到response的text属性上面,最后调用我们上面调用send方法传入的回调函数。done( status, statusText, responses, xhr.getAllResponseHeaders() );

总之:

(1)ajaxTransport只是返回一个具有send,abort等方法的对象,调用这个对象的send方法就相当于真正调用了ajax请求!请求完成以后会把所有的信息传入到send调用的时候指定的回调函数中,其中包括status, statusText, responses, xhr.getAllResponseHeaders() 进而完成回调!

(2)我这里要特别强调一点:当通过jQuery发送ajax实现跨域请求的时候,这时候是不会发送X-Requested-With头的,即使你明确通过heads添加你也会发现不会发送这个HTTP头!哪怕只有端口号不同,协议相同,域名相同也不会发送!其实从代码中也是很容易知道的: !options.crossDomain && !headers["X-Requested-With"]如果是跨域那么crossDomain肯定是true,即使自己不设置jquery也会给你设置,那么这if就不会执行,也就是不会设置 ["X-Requested-With"]头!

done方法源码:

注意:如果是通过上面这个通用的ajaxTransport获取到的对象调用的send方法,那么最后返回的response格式为{text:xhr.responseText}。当然,如果是其它的数据类型可能直接通过自己特有的ajaxTransport完成数据发送和回调了,而不会通过通用的ajaxTransport。这个逻辑在inspectPrefiltersOrTransports中很容易就能看到!因为先处理dataType[0]然后才处理dataType["*"]!

function done( status, nativeStatusText, responses, headers ) {
			var isSuccess, success, error, response, modified,
				statusText = nativeStatusText;
			// Called once
			if ( state === 2 ) {
				return;
			}
			// State is "done" now
			state = 2;
			// Clear timeout if it exists
			if ( timeoutTimer ) {
				clearTimeout( timeoutTimer );
			}
			// Dereference transport for early garbage collection
			// (no matter how long the jqXHR object will be used)
			transport = undefined;
			// Cache response headers
			responseHeadersString = headers || "";
			// Set readyState
			jqXHR.readyState = status > 0 ? 4 : 0;
			// Determine if successful
			isSuccess = status >= 200 && status < 300 || status === 304;
			// Get response data
			if ( responses ) {
				response = ajaxHandleResponses( s, jqXHR, responses );
			}
			// Convert no matter what (that way responseXXX fields are always set)
			response = ajaxConvert( s, response, jqXHR, isSuccess );
			// If successful, handle type chaining
			if ( isSuccess ) {
				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
				if ( s.ifModified ) {
					modified = jqXHR.getResponseHeader("Last-Modified");
					if ( modified ) {
						jQuery.lastModified[ cacheURL ] = modified;
					}
					modified = jqXHR.getResponseHeader("etag");
					if ( modified ) {
						jQuery.etag[ cacheURL ] = modified;
					}
				}
				// if no content
				if ( status === 204 || s.type === "HEAD" ) {
					statusText = "nocontent";
				// if not modified
				} else if ( status === 304 ) {
					statusText = "notmodified";
				// If we have data, let's convert it
				} else {
					statusText = response.state;
					success = response.data;
					error = response.error;
					isSuccess = !error;
				}
			} else {
				// We extract error from statusText
				// then normalize statusText and status for non-aborts
				error = statusText;
				if ( status || !statusText ) {
					statusText = "error";
					if ( status < 0 ) {
						status = 0;
					}
				}
			}
			// Set data for the fake xhr object
			jqXHR.status = status;
			jqXHR.statusText = ( nativeStatusText || statusText ) + "";
			// Success/Error
			if ( isSuccess ) {//Deferred中封装了三个Callbacks对象done,fail,progress如果resolveWith那么done中函数全部调用!
				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
			} else {
				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
			}
			// Status-dependent callbacks
			jqXHR.statusCode( statusCode );
			statusCode = undefined;
			if ( fireGlobals ) {
				globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
					[ jqXHR, s, isSuccess ? success : error ] );
			}
			// Complete
			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
			if ( fireGlobals ) {
				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
				// Handle the global AJAX counter
				if ( !( --jQuery.active ) ) {
					jQuery.event.trigger("ajaxStop");
				}
			}
		}

		return jqXHR;
	}

note:done方法到底在干嘛。

(1)他作为ajax的回调函数处理,他首先会把返回的数据如通用ajaxTransport返回的responses={text:xhr.responseText}封装到jqXHR对象上去,形成jqXHR["responseText"]=xhr.responseText!于是在回调函数里面我们就可以通过jqXHR获取到服务器端返回的数据!

(2)把处理后的数据返回来,如通过通用ajaxTransport返回的responses["text"],也就是得到服务器返回的真正的数据,这个数据是"string"!

(3)ajaxConverter在干嘛?因为第二步返回的数据是string,但是用户通过dataType指定了自己需要的数据类型,ajaxConverter就是把我们获取到的string类型数据转换为用户通过dataType指定的数据类型!

status >= 200 && status < 300 || status === 304;表示请求成功了!

我们首先弄懂ajax中的resolveWith等逻辑,见下面的测试用例:

var deferred=jQuery.Deferred();
var jqXHR={};
function f1()
{
	alert("f1");
}
function f2()
{
	alert("f2");
}
function f3()
{
	alert("f3");
}
//jqXHR具有了promise所有的属性和方法,同时为返回的这个
//增强的jqXHR对象对应的成功回调数组添加了两个回调函数f1,f2
deferred.promise( jqXHR ).done(f1).done(f2);
//jqXHR对象的success方法相当于jqXHR的done方法
//说明通过jqXHR通过success方法添加进去的函数在
//Deferred调用resolve时候也会调用!
jqXHR.success = jqXHR.done;
//通过success方法添加一个回调函数
jqXHR.success(f3);
//弹出[f1,f2,f3]
deferred.resolve();
//代码片段1:
//for ( i in { success: 1, error: 1, complete: 1 } ) {
//			jqXHR[ i ]( s[ i ] );
//		}

//代码片段2:
//deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
//源码中这一句代码的作用:让通过Deferred对象中的成功回调函数全部执行,通过代码片段1
//可以知道:通过jqXHR.success(s[i])把我们自己设置的成功回调函数全部放在Deferred
//对象对应done所对于的Callback中,所以当调用deferred对象的resolveWith时候
//我们自己传送的success方法就会被执行!

//代码片段3:
//var completeDeferred = jQuery.Callbacks("once memory")
//	deferred.promise( jqXHR ).complete = completeDeferred.add;
//这也就是说:我们通过complete方法添加的函数放在了completeDeferred中
//而completeDeferred对应于一个Callback对象,添加的函数数组如何被调用呢?
//completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
//所以Callback对象的调用还是通过原始的fireWith,因为这里是Callback没有done方法等!
通过上面测试代码片段你应该理解下面几个部分:
completeFunction/Array类型

指定请求完成(无论成功或失败)后需要执行的回调函数。该函数还有两个参数:一个是jqXHR对象,一个是表示请求状态的字符串('success'、 'notmodified'、 'error'、 'timeout'、 'abort'或'parsererror')。这是一个Ajax事件从jQuery 1.5开始,该属性值可以是数组形式的多个函数,每个函数都将被回调执行。

successFunction/Array类型

指定请求成功后执行的回调函数。该函数有3个参数:请求返回的数据、响应状态字符串、jqXHR对象。从jQuery 1.5开始,该属性值可以是数组形式的多个函数,每个函数都将被回调执行。

errorFunction/Array类型

指定请求失败时执行的回调函数。该函数有3个参数:jqXHR对象、 请求状态字符串(null、 'timeout'、 'error'、 'abort'和'parsererror')、 错误信息字符串(响应状态的文本描述部分,例如'Not Found'或'Internal Server Error')。这是一个Ajax事件。跨域脚本和跨域JSONP请求不会调用该函数。从jQuery 1.5开始,该属性值可以是数组形式的多个函数,每个函数都将被回调执行。

上面的success,error,complete都是添加到jqXHR对象相应的属性当中。success对应于jqXHR的done方法对应的回调数组,通过resolveWith调用;error对应于jqXHR的fail对应的回调数组,通过rejectWith调用;而complete是通过保存在Callbacks里面而不是Deferred里面,他是不管成功与否都是会调用的,他的调用是通过fireWith这种方法完成的!(可以参考我的关于Deferred和Callbacks对应的源码分析部分),我把这一部分源码附带上:

deferred = jQuery.Deferred(),
completeDeferred = jQuery.Callbacks("once memory")
//complete通过保存在Callbacks中实现
deferred.promise( jqXHR ).complete = completeDeferred.add;
//success对应于done
jqXHR.success = jqXHR.done;
//error对应于fail
jqXHR.error = jqXHR.fail;
//resolveWith调用通过done方法或者success添加的回调函数
deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
//rejectWith调用通过fail或者error添加的回调函数
deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
//compelte添加的函数是通过fireWith实现,因为他是Callbacks而不是Deferred对象!
completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );

上面的代码中,window怎么有callbackName呢,看下面的代码,这是jQuery为我们自动生成的一个函数:

jQuery.ajaxSetup({
	jsonp: "callback",
	jsonpCallback: function() {
		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
		this[ callback ] = true;
		return callback;
	}
});
note:这一句代码使得最终的options对象具有了两个属性,一个是jsonp,一个是jsonpCallback,这是jQuery为我们内置的两个属性,可以通过jQuery.ajaxSettings.jsonpCallback打印看到函数签名!如果打印的时候在后面加上一个括号表示函数调用,这时候就会返回一个函数名,函数名如 jQuery111107973217158578336_1446447729232这样的字符串!如果我们发送jsonp请求的时候没有指定这两个参数那么结果就是如下的URL:http://localhost:8080/qinl/a.action?callback=jQuery111107973217158578336_1446447729232,服务器端通过通过获取到callback后这个jQuery111107973217158578336_1446447729232函数名,然后返回的字符串为jQuery111107973217158578336_1446447729232("hello, I am back!"),返回到客户端以后就相当于直接调用了这个函数!于是responseContainer就相当于回调时候的实参,这个实参是服务器端发送过来的数据!
我们再次分析一下下面这一段代码片段:

s.converters["script json"] = function() {
			if ( !responseContainer ) {
				jQuery.error( callbackName + " was not called" );
			}
			return responseContainer[ 0 ];
		};
note:这一段代码的作用就是在最终的options的converters中添加了一段相应格式的处理函数,我们先看看converters里面放的是什么:

converters: {
			// Convert anything to text
			"* text": String,
			// Text to html (true = no transformation)
			"text html": true,
			// Evaluate text as a json expression
			"text json": jQuery.parseJSON,
			// Parse text as xml
			"text xml": jQuery.parseXML
		}
note:现在说说上面那段代码的作用,他相当与告诉jQuery,如果用户传入的参数是dataType="script json"那么我们就用后面这个函数处理。我们可以看到对于"text json"用了jQuery.parseJSON从而把服务器端的返回数据转化为JSON。那么传入的dataType="script json"会怎么处理呢?如果服务器端没有返回数据,那么jQuery就抛出一个异常,如果jQuery返回了数据那么就直接把服务器返回的数据返回! 这就是对"script json"的处理逻辑!这个函数会在ajaxConvert函数中被调用!
jsonpString类型

重写JSONP请求的回调函数名称。该值用于替代"url?callback=?"中的"callback"部分。服务器用request.getParameter获取到!服务器返回之为jsonpCallback("服务器要传递给浏览器的数组")。

jsonpCallbackString/Function类型

为JSONP请求指定一个回调函数名。这个值将用来取代jQuery自动生成的随机函数名。

从jQuery 1.5开始,你也可以指定一个函数来返回所需的函数名称。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值