从jQuery中剥离出$.ajax函数

bachue posted @ 2010年12月10日 20:41 in JavaScript with tags ajax javascript js post jquery get getjson getscript load appspot , 5115 阅读

作为开源社区的PHP框架的内置Ajax模块,我们需要一个功能超强大的Ajax函数,作为一个通用函数。Ajax模块是我负责的,这种函数如果真要我写,恐怕以我的能力是很难做到完美的,毕竟Ajax涉及的知识点非常散,还有大量HTTP协议的知识,我并不全懂,还有各种浏览器兼容问题的存在,防不胜防。就算花一个月去做,恐怕也只能做出个可能存在大量隐蔽BUG的作品。于是,我决定直接从开源产品中找,也就是jQuery的$.ajax函数。

我是个信奉极简约的人,不喜欢为了一个$.ajax就把整个jQuery文件全部包含进来,太大了,70多KB的文件,换成56K/bps的猫,全速也至少10秒(一般我不考虑什么gzip压缩,因为不想把它作为偷懒的理由,虽然号称压缩后只有24K),我不能接受这种速度,所以我就决定从中提取,把jQuery中的$.ajax和一些附属的函数剥离出来,单独形成一个ajax函数。

花了半天时间,非常顺利,jQuery源码并不难懂,至少一定比Linux Kernel源码好懂。不过首个版本没有提取所有功能。比方说Cache功能还没有,因为不了解Ajax的Cache究竟是个什么机制,全局Ajax事件没有,因为感觉意义不大,如果要,就必须剥离$.trigger,这个函数不是很小,剥离就可能导致最终文件变大,有点得不偿失。

可能存在Bug,请注意,当需要解析JSON时,本类中没有自带JSON解析函数,请务必提供JSON.parse()函数用于解码。

/*
 * Class: Ajax
 * Author: Bachue Zhou
 * Description: Deliver Ajax module from jquery 
 * Date: 12/08/2010 
 * Depend on: JSON.parse() Must be provided!
 * Version: 0.02
 */

var Ajax;
if(!Ajax||typeof(Ajax)!="object")
{
    Ajax={};
}

Ajax.ajaxSettings={ //init ajax setting
	url: location.href,
	type: "GET",
	contentType: "application/x-www-form-urlencoded",
	processData: true,
	async: true,
	/*
	timeout: 0,
	data: null,
	username: null,
	password: null,
	traditional: false,
	*/
	xhr: function() {
		//return ajax object. must be comparable with fuck ie.
		try{return new XMLHttpRequest();}
		catch(e){try{return new ActiveXObject("Msxml2.XMLHTTP");}
		catch(e){return new ActiveXObject("Microsoft.XMLHTTP");}}
	},
	accepts: {
		xml: "application/xml, text/xml",
		html: "text/html",
		script: "text/javascript, application/javascript",
		json: "application/json, text/javascript",
		text: "text/plain",
		_default: "*/*"
	}
};

Ajax.setup=function( settings ) 
{
	Ajax._extend( Ajax.ajaxSettings, settings );
};

Ajax.send=function(origSettings)
{
	function getType( obj ) 
	{
		return obj === null ? String( obj ) : class2type[ toString.call(obj) ] || "object";
	}
	
	function isFunction( obj )
	{
		return getType(obj) === "function";
	}

	function isArray( obj ) 
	{
		return getType(obj) === "array";
	}
	
	function isEmptyObject( obj )
	{
		for ( var name in obj ) 
		{
			return false;
		}
		return true;
	}
		
	function param(a)
	{
		var s = [],name,
			add = function( key, value ) {
				// If value is a function, invoke it and return its value
				value = isFunction(value) ? value() : value;
				s[ s.length ] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
			};
		
		// If an array was passed in, assume that it is an array of form elements.
		if ( isArray(a) ) 
		{
			// Serialize the form elements
			for(name in a)
			{
				add( name, a[name] );
			}
		}
		else
		{
			// If traditional, encode the "old" way (the way 1.3.2 or older
			// did it), otherwise encode params recursively.
			for ( var prefix in a ) 
			{
				buildParams( prefix, a[prefix], add );
			}
		}

		// Return the resulting serialization
		return s.join("&").replace(r20, "+");
	}
	
	function buildParams( prefix, obj, add )
	{
		if ( isArray(obj) && obj.length )
		{
			// Serialize array item.
			for(var i in obj)
			{
				var v=obj[i];
				if ( rbracket.test( prefix ) ) 
				{
					// Treat each array item as a scalar.
					add( prefix, v );
				} 
				else 
				{
					// If array item is non-scalar (array or object), encode its
					// numeric index to resolve deserialization ambiguity issues.
					// Note that rack (as of 1.0.0) can't currently deserialize
					// nested arrays properly, and attempting to do so may cause
					// a server error. Possible fixes are to modify rack's
					// deserialization algorithm or to provide an option or flag
					// to force array serialization to be shallow.
					buildParams( prefix + "[" + ( typeof v === "object" || isArray(v) ? i : "" ) + "]", v, add );
				}	
			}	
		} 
		else if ( obj !== null && typeof(obj) === "object" ) 
		{
			if ( isEmptyObject( obj ) ) 
			{
				add( prefix, "" );
			} 
			else 
			{
			// Serialize object item.
				for(var k in obj)
				{
					var v=obj[k];
					buildParams( prefix + "[" + k + "]", v, add );
				}
			}
		} 
		else 
		{
			// Serialize scalar item.
			add( prefix, obj );
		}
	}

	function globalEval( data )
	{
		if ( data && rnotwhite.test(data) ) {
			// Inspired by code by Andrea Giammarchi
			// http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html
			var head = document.getElementsByTagName("head")[0] || document.documentElement,
				script = document.createElement("script");

			script.type = "text/javascript";

			if ( scriptEval ) {
				script.appendChild( document.createTextNode( data ) );
			} else {
				script.text = data;
			}

			// Use insertBefore instead of appendChild to circumvent an IE6 bug.
			// This arises when a base node is used (#2709).
			head.insertBefore( script, head.firstChild );
			head.removeChild( script );
		}
	}
	
	//return current time
	function now()
	{
		return (new Date()).getTime();
	}
	
	function handleError( s, xhr, status, e ) {
		// If a local callback was specified, fire it
		if ( s.error ) {
			s.error.call( s.context, xhr, status, e );
		}
	}
	
	function handleSuccess( s, xhr, status, data )
	{
		// If a local callback was specified, fire it and pass it the data
		if ( s.success )
		{
			s.success.call( s.context, data, status, xhr );
		}
	}

	function handleComplete( s, xhr, status ) 
	{
		// Process result
		if ( s.complete )
		{
			s.complete.call( s.context, xhr, status );
		}
	}
	
	// Determines if an XMLHttpRequest was successful or not
	function httpSuccess( xhr )
	{
		try {
			// IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450
			return !xhr.status && location.protocol === "file:" ||
				xhr.status >= 200 && xhr.status < 300 ||
				xhr.status === 304 || xhr.status === 1223;
		} catch(e) {}

		return false;
	}

	// Determines if an XMLHttpRequest returns NotModified
	function httpNotModified( xhr, url ) 
	{
		var lastModified = xhr.getResponseHeader("Last-Modified"),
			etag = xhr.getResponseHeader("Etag");

		if ( lastModified ) {
			lastModified[url] = lastModified;
		}

		if ( etag ) {
			etag[url] = etag;
		}

		return xhr.status === 304;
	}

	function httpData( xhr, type, s ) 
	{
		var ct = xhr.getResponseHeader("content-type") || "",
			xml = type === "xml" || !type && ct.indexOf("xml") >= 0,
			data = xml ? xhr.responseXML : xhr.responseText;

		if ( xml && data.documentElement.nodeName === "parsererror" ) 
		{
			throw "parsererror";
		}

		// Allow a pre-filtering function to sanitize the response
		// s is checked to keep backwards compatibility
		if ( s && s.dataFilter ) 
		{
			data = s.dataFilter( data, type );
		}

		// The filter can actually parse the response
		if ( typeof data === "string" ) 
		{
			// Get the JavaScript object, if JSON is used.
			if ( type === "json" || !type && ct.indexOf("json") >= 0 ) 
			{
				data = JSON.parse( data );
			}
			else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) 
			{
				globalEval( data );
			}
		}

		return data;
	}
	
	function noop()
	{
	}
	
	var root = document.documentElement,
		script = document.createElement("script"),
		id = "script" + now(),
		scriptEval=false;
	
	script.type = "text/javascript";
	try {
		script.appendChild( document.createTextNode( "window." + id + "=1;" ) );
	} catch(e) {}

	root.insertBefore( script, root.firstChild );

	// Make sure that the execution of code works by injecting a script
	// tag with appendChild/createTextNode
	// (IE doesn't support this, fails, and uses .text instead)
	if ( window[ id ] ) {
		scriptEval = true;
		delete window[ id ];
	}
	root.removeChild( script );
	
	var class2type = {
			"[object Array]":"array",
			"[object Boolean]":"boolean",
			"[object Date]":"date",
			"[object Function]":"function",
			"[object Number]":"number",
			"[object Object]":"object",
			"[object RegExp]":"regexp",
			"[object String]":"string"
			},
				
	jsc = now(),
	rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
	rselectTextarea = /^(?:select|textarea)/i,
	rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
	rnoContent = /^(?:GET|HEAD)$/,
	rbracket = /\[\]$/,
	jsre = /\=\?(&|$)/,
	rquery = /\?/,
	rts = /([?&])_=[^&]*/,
	rurl = /^(\w+:)?\/\/([^\/?#]+)/,
	r20 = /%20/g,
	rhash = /#.*$/,
	rnotwhite = /\S/,
	
	lastModified={},
	etag={},

	s=Ajax._extend(Ajax.ajaxSettings, origSettings),
		jsonp, status, data, type = s.type.toUpperCase(), noContent = rnoContent.test(type);
	
	//erase some meaningless symbol
	s.url = s.url.replace(rhash,"");
	
	// Use original (not extended) context object if it was provided
	s.context = origSettings && origSettings.context !== null ? origSettings.context : s;
	
	// convert data if not already a string
	if ( s.data && s.processData && typeof s.data !== "string" ) 
	{
		s.data = param( s.data );
	}
	
	if ( s.dataType === "jsonp" )
	{
		if ( type === "GET" )
		{
			//test whether there is '=?' in URL, if not, splice it
			if ( !jsre.test( s.url ) ) 
			{
				s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?";
			}
		} 
		//test data
		else if ( !s.data || !jsre.test(s.data) ) 
		{
			s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?";
		}
		//set datatype to json
		s.dataType = "json";
	}
	
	if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) 
	{
		jsonp = s.jsonpCallback || ("jsonp" + jsc++);

		// Replace the =? sequence both in the query string and the data
		if ( s.data )
		{
			s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1");
		}

		s.url = s.url.replace(jsre, "=" + jsonp + "$1");

		// We need to make sure
		// that a JSONP style response is executed properly
		s.dataType = "script";

		// Handle JSONP-style loading
		var customJsonp = window[ jsonp ];

		window[ jsonp ] = function( tmp ) 
		{
			if ( isFunction( customJsonp ) ) 
			{
				customJsonp( tmp );
			} 
			else 
			{
				// Garbage collect
				window[ jsonp ] = undefined;

				try
				{
					delete window[ jsonp ];
				}
				catch( jsonpError ){}
			}

			data = tmp;
			handleSuccess( s, xhr, status, data );
			handleComplete( s, xhr, status, data );
			
			if ( head ) 
			{
				head.removeChild( script );
			}
		};
	}
	
	// If data is available, append data to url for GET/HEAD requests
	if ( s.data && noContent )
	{
		s.url += (rquery.test(s.url) ? "&" : "?") + s.data;
	}
	
	// Matches an absolute URL, and saves the domain
	var parts = rurl.exec( s.url ),
		remote = parts && (parts[1] && parts[1].toLowerCase() !== location.protocol || parts[2].toLowerCase() !== location.host);
	
	if ( s.dataType === "script" && type === "GET" && remote )
	{
		var head = document.getElementsByTagName("head")[0] || document.documentElement;
		var script = document.createElement("script");
		if ( s.scriptCharset )
		{
			script.charset = s.scriptCharset;
		}
		script.src = s.url;

		// Handle Script loading
		if ( !jsonp )
		{
			var done = false;

			// Attach handlers for all browsers
			script.onload = script.onreadystatechange = function() 
			{
				if ( !done && (!this.readyState ||
						this.readyState === "loaded" || this.readyState === "complete") ) 
				{
					done = true;
					handleSuccess( s, xhr, status, data );
					handleComplete( s, xhr, status, data );

					// Handle memory leak in IE
					script.onload = script.onreadystatechange = null;
					if ( head && script.parentNode )
					{
						head.removeChild( script );
					}
				}
			};
		}

		// Use insertBefore instead of appendChild  to circumvent an IE6 bug.
		// This arises when a base node is used (#2709 and #4378).
		head.insertBefore( script, head.firstChild );

		// We handle everything using the script element injection
		return undefined;
	}
	
	var requestDone = false;

	// Create the request object
	var xhr = s.xhr();

	if ( !xhr ) 
	{
		return;
	}

	// Open the socket
	// Passing null username, generates a login popup on Opera (#2865)
	if ( s.username )
	{
		xhr.open(type, s.url, s.async, s.username, s.password);
	} 
	else
	{
		xhr.open(type, s.url, s.async);
	}
	
	// Need an extra try/catch for cross domain requests in Firefox 3
	try
	{
		// Set content-type if data specified and content-body is valid for this type
		if ( (s.data !== null && !noContent) || (origSettings && origSettings.contentType) ) 
		{
			xhr.setRequestHeader("Content-Type", s.contentType);
		}

		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
		if ( s.ifModified ) 
		{
			if ( lastModified[s.url] )
			{
				xhr.setRequestHeader("If-Modified-Since", lastModified[s.url]);
			}

			if ( etag[s.url] )
			{
				xhr.setRequestHeader("If-None-Match", etag[s.url]);
			}
		}

		// Set header so the called script knows that it's an XMLHttpRequest
		// Only send the header if it's not a remote XHR
		if ( !remote ) 
		{
			xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
		}

		// Set the Accepts header for the server, depending on the dataType
		xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ?
			s.accepts[ s.dataType ] + ", */*; q=0.01" :
			s.accepts._default
		);
	} catch( headerError ) {}
	
	// Allow custom headers/mimetypes and early abort
	if ( s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false ) 
	{
		// close opended socket
		xhr.abort();
		return false;
	}
	
	var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) 
	{
		// The request was aborted
		if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" )
		{
			// Opera doesn't call onreadystatechange before this point
			// so we simulate the call
			if ( !requestDone )
			{
				handleComplete( s, xhr, status, data );
			}

			requestDone = true;
			if ( xhr )
			{
				xhr.onreadystatechange = noop;
			}

		// The transfer is complete and the data is available, or the request timed out
		} 
		else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) 
		{
			requestDone = true;
			xhr.onreadystatechange = noop;

			status = isTimeout === "timeout" ?
				"timeout" :
				!httpSuccess( xhr ) ?//really success?
					"error" :
					s.ifModified && httpNotModified( xhr, s.url ) ?
						"notmodified" :
						"success";

			var errMsg;

			if ( status === "success" ) 
			{
				// Watch for, and catch, XML document parse errors
				try 
				{
					// process the data (runs the xml through httpData regardless of callback)
					data = httpData( xhr, s.dataType, s );//judge responseXML or responseText
				} 
				catch( parserError ) 
				{
					status = "parsererror";
					errMsg = parserError;
				}
			}

			// Make sure that the request was successful or notmodified
			if ( status === "success" || status === "notmodified" ) 
			{
				// JSONP handles its own success callback
				if ( !jsonp ) 
				{
					handleSuccess( s, xhr, status, data );
				}
			} 
			else 
			{
				handleError( s, xhr, status, errMsg );
			}

			// Fire the complete handlers
			if ( !jsonp )
			{
				handleComplete( s, xhr, status, data );
			}

			if ( isTimeout === "timeout" ) {
				xhr.abort();
			}

			// Stop memory leaks
			if ( s.async ) {
				xhr = null;
			}
		}
	};

	// Override the abort handler, if we can (IE 6 doesn't allow it, but that's OK)
	// Opera doesn't fire onreadystatechange at all on abort
	try 
	{
		var oldAbort = xhr.abort;
		xhr.abort = function() 
		{
			if ( xhr ) {
				// oldAbort has no call property in IE7 so
				// just do it this way, which works in all
				// browsers
				Function.prototype.call.call( oldAbort, xhr );
			}

			onreadystatechange( "abort" );
		};
	} catch( abortError ) {}

	// Timeout checker
	if ( s.async && s.timeout > 0 ) 
	{
		setTimeout(function()
		{
			// Check to see if the request is still happening
			if ( xhr && !requestDone ) 
			{
				onreadystatechange( "timeout" );
			}
		}, s.timeout);
	}

	// Send the data
	try {
		xhr.send( noContent || s.data === null ? null : s.data );

	} 
	catch( sendError ) 
	{
		handleError( s, xhr, null, sendError );

		// Fire the complete handlers
		handleComplete( s, xhr, status, data );
	}

	// firefox 1.5 doesn't fire statechange for sync requests
	if ( !s.async )
	{
		onreadystatechange();
	}

	// return XMLHttpRequest to allow aborting the request etc.
	return xhr;
};

//merge multi objects into the first object and return
Ajax._extend=function ()
{
	var target = arguments[0] || {},i,length = arguments.length,options,src,copy,clone,name;
	for(i=1;i<length;++i)
	{
		// Only deal with non-null/undefined values
		if ((options=arguments[i])!==null)
		{
			// Extend the base object
			for(name in options)
			{
				src=target[ name ];
				copy=options[ name ];
				
				// Prevent never-ending loop
				if (target===copy)
				{
					continue;
				}
				
				// Recurse if we're merging objects
				if(typeof(copy)=="object")
				{
					clone=(src && typeof(src)=="object"?src:{});
					target[name]=Ajax._extend(clone,copy);
				}
				// Don't bring in undefined values
				else if(copy !== undefined)
				{
					target[name ] = copy;
				}
			}
		}
	}
	// Return the modified object
	return target;
};

最终文件共8.4K,56K/bps猫只需1秒多一点,显然是可以接受的。min版本下载

这个函数其实还有一些附带函数,比方说post,get,getScript,getJSON,load之类的,我全让我的组员去完成了,其实非常简单。至于Cache机制的剥离,也让他们作为一个附加题去做了,我寝室最近网速很糟,笔记本电脑有线网卡损坏,总之RP不够,收集这方面的资料很有困难。

最后缅怀被墙的AppSpot,上推再一次变的困难,希望有个男人能勇敢的站出来为此事负责!

java程序员 说:
2011年7月02日 19:12

你好!我也是一个程序员,但是对js一点也不太懂,最近我也在做一个普通的网站,你能不能把你们剥离的成果给我一份啊。

java程序员 说:
2011年7月02日 19:15

我的email是422053362@qq.com.我们能不能多交流一下?

Avatar_small
bachue 说:
2011年7月05日 12:03

@java程序员: 真糟糕,我的组员没有按照我的要求完成任务。因此所有代码均已经列在上面,你可以任意修改使用这些代码。我的Gmail是bachue.shu#gmail.com,你可以发邮件给我。

有意义 说:
2013年1月26日 21:02

还是谷歌好啊,我百度了半天也没找到如何剥离jquery的文章,更比直接找剥离好ajax的了。。
使用别人的研究成果,是站在巨人肩上的表现。。感谢老大了。

Avatar_small
bachue 说:
2013年1月27日 09:39

@有意义: 天哪 这是我几年前写的东西啊 AJAX的实现后来jQuery好像完全重写了 恐怕你要重新来了 话说现在其实时代也改变了 你整个jQuery放进去 再加上所有基于jQuery的库 加个min 加个gzip 其实也不怎么慢 而且只要下载一次 以后全部依靠缓存 没必要搞什么剥离了


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter