原生AJAX入门讲解(含实例)

相对于jQuery、YUI以及其他一些类库的AJAX封装,原生JS的AJAX显得那么的尴尬,兼容性不好,要记很多的方法属性,调用不便捷,代码臃肿…但我还是想说,原生JS才是最根本最底层的知识(虽然实际项目中我也是以jQuery AJAX为主,为什么?高效!),求木之长者,必固其根本。
什么是AJAX? 它的优点劣势是什么?
Asynchronous JavaScript and XML(异步JavaScript和XML),是一种创建交互式网页应用的网页开发技术。简单的说它是多种技术的组合,目的就是让前台的数据交互变得更快捷,不用刷新页面就可以完成数据的更新。关于它的概念,止于此。想了解更多,请前往:http://zh.wikipedia.org/zh/AJAX
优点很明显,利于用户体验,不会打断用户的操作,在不刷新页面的情况下更新内容,减小服务器压力也是它很硬性的一个优点;而缺点,除了倍受追捧的SEO问题,安全问题、前进后退(这个虽然可以用其他方法解决,但AJAX本身的机制还是没变)、破坏程序的异常机制以及对新兴手持设备支持不完美的问题都是它现存的一些缺点。人无完人,AJAX也是如此,我们并不能因为它有缺点而摒弃它。
什么地方需要AJAX?
其实这是一个太宽泛的问题,各人各项目都有不同的用处,依我的经验与理解,AJAX应该用于小面积更新数据而不希望整个页面刷新的情况下使用。比如对用户名或者注册邮箱等数据判断、内容选项卡内容、弹出的登录注册窗口以及用户提交信息后的反馈信息等等。
实践出真知!
崇尚思考加实践,我坚信这是深入任何一门技术的必备法宝。下面,我就以一个常用的验证用户是否使用的实例,浅尝辄止的讲解一下基础的AJAX。查看样例演示
验证用户名这种数据判断,不用多说了,会一点点前端的人都知道是必须的。最传统的模式可能是输入信息,然后用户点提交后alert出一个窗口,告诉用户XXX用户名已被注册,请重新输入!我讨厌极丑的alert框!我想大多用户也是一样。此时,AJAX就可以华丽登场了。当用户输入完名字后,在表单外任何地方点一下(失去焦点),AJAX就迅速把用户输入的信息反馈到服务器端判断,并迅速返回一个信息告知用户输入的昵称是否可用。如此,我们需要一个前台的输入表单,代码如下:

<p> </p>

另外,我们还需要一个判断输入昵称是否存在的后端页面(本文以PHP为例,这部分不用细究):

...
if(isset($_GET['entryname'])){
	$entryname=$_GET['entryname'];
}else{
	$entryname='DATA NULL';
}
$sql=sprintf("select * from i_test_ajax where nickname='%s'",$entryname);
$res=$iajax->query($sql);
//sleep只是为了展示readState==1时的效果
sleep(1);
if(($res->num_rows)>0){
	echo "抱歉!此昵称已存在 :(";
}else{
	echo "恭喜!此昵称可注册 :)";
}
...

如此,万事俱备,只欠东风,剩下的就交给AJAX来处理了。
XMLHttpRequest,不得不提的一个对象,AJAX最核心也是最底层的对象。可悲哀的是,它是W3C的一个标准,但微软IE一直很自我(微软IE)。怎么办?当然是用一个方法和谐掉它们。微软IE支持ActiveXObject(‘Microsoft.XMLHTTP’)对象,这样就简单了:

//兼容的XMLHttpRequest对象
IXHR: function(){
	if(window.ActiveXObject){
			XHR=new ActiveXObject('Microsoft.XMLHTTP');
		}else if(window.XMLHttpRequest){
			XHR=new XMLHttpRequest();
		}else{
        	return null;
        }
}

兼容的XMLHttpRequest对象实现了,接下来写一个简单的onblur事件,即当输入值后,表单失去焦点后开始判断并迅速回馈一个信息到前台。代码如下:

//触发焦点时执行
document.forms['iform'].nickname.onblur = function(){
    //输入的值
    var val=document.forms['iform'].nickname.value;
    //对用户名的判断
    if(!/^[a-zA-Z0-9_]{3,16}$/.test(val)){
		alert('请输入3~16位由英文、数字、下划线组成的昵称!');
		return false;
    }
    //初始化一下XHR
    iBase.IXHR();
    //原来需要新打开的判断页面用GET使用异步
	XHR.open('GET','/demo/ajax/iajax20110306_query.php?entryname='+val,true);
	//与readyState属性有关,当readyState改变时它才会触发
	XHR.onreadystatechange=returnFun;
	//异步处理完成后发送数据出去(比如某些需要在焦点事件后再执行的)
	XHR.send(null);
}

解释一下AJAX部分的代码。iBase.IXHR(),初始化XHR,以保证XMLHttpRequest对象的兼容。接下来,通过以GET的方式,异步发送到/demo/ajax/iajax20110306_query.php页面进行验证。有人会问什么是GET,GET是从服务器上请求数据,GET方法就是把数据参数队列加到一个URL上,值和表单是一一对应的,比如本文的entryname=val。这个概念可能属于后台程序的范畴,不在此细说。然后,我们需要用到一个onreadystatechange事件属性,这个属性是用来存储函数(或函数名),每当readyState属性改变时,就会调用该函数,即本文中的returnFun;最后,我们还要发送一个数据到服务器,send属性一般用于数据交换,而本文只是一个简单的验证判断,所以,send一个空值。
基本的判断与数据发送完成了,接下来还剩一个关键的信息获取,即returnFun。先看代码:

function returnFun(){
    //当send()已调用,正在发送请求时,显示Loading...
	if(XHR.readyState==1){
		iBase.Id('tips').innerHTML='Loding...';
	}else if(XHR.readyState==4){
		//当响应内容解析完成,可以调用时
		//更缜密,再判断一下status是否成功
		if(XHR.status==200){
			//responseText为返回的文本
			iBase.Id('tips').innerHTML=XHR.responseText;
		}
		//使用完请销毁,避免内存泄露
		XHR=null;
	}
}

此函数是用来通过判断readyState及status状态也及时反馈给用户相应的提示信息。readyState有五种状态:
  0 (未初始化): (XMLHttpRequest)对象已经创建,但还没有调用open()方法;
  1 (载入):已经调用open() 方法,但尚未发送请求;
  2 (载入完成): 请求已经发送完成;
  3 (交互):可以接收到部分响应数据;
  4 (完成):已经接收到了全部数据,并且连接已经关闭。
如此一来,你应该就能明白readyState的功能,而status实际是一种辅状态判断,只是status更多是服务器方的状态判断。关于status,由于它的状态有几十种,我只列出平时常用的几种:
  100——客户必须继续发出请求
  101——客户要求服务器根据请求转换HTTP协议版本
  200——成功
  201——提示知道新文件的URL
  300——请求的资源可在多处得到
  301——删除请求数据
  404——没有发现文件、查询或URl
  500——服务器产生内部错误
至此,一个简单的AJAX验证实例就完成了:查看样例演示

关于AJAX的基础介绍与实例就这么多,关键还是在于自己的实践与思考。其实这其中涉及知识并不复杂,若有后端程序的基础,学起AJAX会更加容易,关键是要想明白其中的逻辑关系。有兴趣的话,可以自己写一个不刷新页面加载新内容的AJAX,或者研究一下jQuery中关于AJAX的封装。

JS实现动态显示当前时间(12/24小时制)

今天又要用到动态显示当前时间,且要求可以切换24/12小时制, 记得很久前写过一个24小时制的,但一时没能找着, 于是又重新写了个,若有需要的朋友自行复制粘贴一份吧.
很简单的一个功能函数,实现方式不多言,用Date()对象获取到当前时间,然后用setTimeout每隔1秒获取最新的时间.
写的过程中碰到过一个小小的问题: 我最初的想法是用setInterval()每隔1秒获取最新时间,可是可以,但setInterval如果放在主函数内部,但导致内存泄漏(至于原因,暂时还没想明白),后来在Rocky的提醒下用setTimeout()才解决内存泄漏问题,感谢Rocky同学:)
核心代码及演示查看样例演示

function nowTime(ev,type){
	/*
	 * ev:显示时间的元素
	 * type:时间显示模式.若传入12则为12小时制,不传入则为24小时制
	 */
	//年月日时分秒
	var Y,M,D,W,H,I,S;
	//月日时分秒为单位时前面补零
	function fillZero(v){
		if(v<10){v='0'+v;}
		return v;
	}
	(function(){
		var d=new Date();
		var Week=['星期天','星期一','星期二','星期三','星期四','星期五','星期六'];
		Y=d.getFullYear();
		M=fillZero(d.getMonth()+1);
		D=fillZero(d.getDate());
		W=Week[d.getDay()];
		H=fillZero(d.getHours());
		I=fillZero(d.getMinutes());
		S=fillZero(d.getSeconds());
		//12小时制显示模式
		if(type && type==12){
			//若要显示更多时间类型诸如中午凌晨可在下面添加判断
			if(H<=12){
				H='上午 '+H;
			}else if(H>12 && H<24){
				H-=12;
				H='下午 '+fillZero(H);
			}else if(H==24){
				H='下午 00';
			}
		}
		ev.innerHTML=Y+'年'+M+'月'+D+'日 '+' '+W+' '+H+':'+I+':'+S;
		//每秒更新时间
		setTimeout(arguments.callee,1000);
	})();
}

原生JS实现淡入淡出效果(fadeIn/fadeOut/fadeTo)

淡入淡出效果,在日常项目中经常用到,可惜原生JS没有类似的方法,而有时小的页面并不值得引入一个jQuery库,所以就自己写了一个,已封装, 有用得着的朋友, 可以直接使用. 代码中另附有一个设置元素透明度的方法, 是按IE规则(0~100)设置, 若改成标准设置方法(0.00~1.00), 下面使用时请考虑浮点精确表达差值.
参数说明:
fadeIn()与fadeOut()均有三个参数,第一个是事件, 必填; 第二个是淡入淡出速度, 正整数, 大小自己权衡, 可选参数; 第三个, 是指定淡入淡出到的透明度值(类似于jQuery中的fadeTo()), 0~100的正整数值, 也是可选参数.
核心代码及演示: 查看样例演示

//淡入效果(含淡入到指定透明度)
function fadeIn(elem, speed, opacity){
	/*
	 * 参数说明
	 * elem==>需要淡入的元素
	 * speed==>淡入速度,正整数(可选)
	 * opacity==>淡入到指定的透明度,0~100(可选)
	 */
    speed = speed || 20;
    opacity = opacity || 100;
	//显示元素,并将元素值为0透明度(不可见)
    elem.style.display = 'block';
    iBase.SetOpacity(elem, 0);
	//初始化透明度变化值为0
    var val = 0;
	//循环将透明值以5递增,即淡入效果
    (function(){
        iBase.SetOpacity(elem, val);
        val += 5;
        if (val <= opacity) {
            setTimeout(arguments.callee, speed)
        }
    })();
}

//淡出效果(含淡出到指定透明度)
function fadeOut(elem, speed, opacity){
	/*
	 * 参数说明
	 * elem==>需要淡入的元素
	 * speed==>淡入速度,正整数(可选)
	 * opacity==>淡入到指定的透明度,0~100(可选)
	 */
    speed = speed || 20;
    opacity = opacity || 0;
    //初始化透明度变化值为0
    var val = 100;
	//循环将透明值以5递减,即淡出效果
    (function(){
        iBase.SetOpacity(elem, val);
        val -= 5;
        if (val >= opacity) {
            setTimeout(arguments.callee, speed);
        }else if (val < 0) {
			//元素透明度为0后隐藏元素
            elem.style.display = 'none';
        }
    })();
}

多组处理, 仅展开一个区块的折叠效果(原生Js和jQ版)

上午在项目中遇到的一个问题, 折腾了半小时才把思路理顺. 需求是, 同一个页面, 有多组(不固定), 每组区块数量不一定一样的小区块. 要求每次只展开一个区块. 实现原理其实很简单, 点击导航, 若它的区块为隐藏, 则展开它, 同时, 隐藏掉同组其他区块; 若它的区块为展开, 则隐藏它, 同时, 展开同组其他区块中的一个. 一开始以为仅仅简单的两个遍历就能搞定. 但事实并非如此. 冷静思考了下, 通过点击的元素取到当前组的相关元素, 再单独处理当前组才合理. 顺着这个思路, 功能终于实现了, 写了原生JS版本, 用同样的思路写了个jQ版本. 时间关系, 写的也比较零散, 就没有封装. 其实, 对这种思路也不是很满意, 感觉太散了, 哪位大师有更好的思路请赐教.

另, 在演示页面中, 点击jQ版区块的标题, 会有一个JS错误, 那是因为获取JS版下的h2时, 我偷了个懒, 把jQ的也遍历进去了. 我想, 实际应用中, 也不会有人同一个效果, 一边用JS一边用jQ吧.
核心代码点此查看样例

//原生JS版本 ***** start
window.onload=function(){
	//共用函数区
	var iBase={
		//document.getElementById
		Id: function(name){return document.getElementById(name)},
		//通过class获取元素
		GetByClass: function(name,tagName,elem){
			var c=[];
			var re=new RegExp('(^|\\s)'+name+'(|\\s$)');
			var e=(elem || document).getElementsByTagName(tagName || '*');
			for(var i=0; i < e.length; i++){
				if(re.test(e[i].className)){
					c.push(e[i]);
				}
			}
			return c;
		},
		//获取样式属性
		AttrStyle: function(elem,attr){
			if(elem.attr){
				return elem.style[attr];
			}else if(elem.currentStyle){
				return elem.currentStyle[attr];
			}else if(document.defaultView && document.defaultView.getComputedStyle){
				attr=attr.replace(/([A-Z])/g,'-$1').toLowerCase();
				return document.defaultView.getComputedStyle(elem,null).getPropertyValue(attr);
			}else{
				return null;
			}
		},
		//获取祖辈元素中符合指定样式的元素
		Parents: function(elem,name){
			var r=new RegExp('(^|\\s)'+name+'(|\\s$)');
			elem=elem.parentNode;
			if(elem!=null){
				return r.test(elem.className) ? elem : iBase.Parent(elem,name) || null;
			}
		},
		//取索引值
		Index: function(cur,obj){
			for(var i=0; i < obj.length; i++){
				if(obj[i]==cur){
					return i;
				}
			}
		}

	}

	//变量定义
	var listBox=iBase.GetByClass('js','div');
	var navItem=iBase.Id('demo').getElementsByTagName('h2');//此处将jQ区块中的h2也取到了,所以页面会有个小小的错误
	var icoItem=null,boxItem=null,boxDisplay=null,elemIndex=null,elemParent=null;
	//初始化展开第一个
	for(var i=0; i < listBox.length;i++){
		iBase.GetByClass('box','div',listBox[i])[0].style.display='block';
		listBox[i].getElementsByTagName('span')[0].innerHTML='-';
	}
	//遍历所有点击项
	for(var i=0; i < navItem.length;i++){
		navItem[i].onclick=function(){
			elemParent=iBase.Parents(this,'js');//获取当前点击所在区块
			navItem=elemParent.getElementsByTagName('h2');//获取当前区块下的点击项
			icoItem=elemParent.getElementsByTagName('span');//获取当前区块下的展开关闭
			boxItem=iBase.GetByClass('box','div',elemParent);//获取需要控制的区块
			elemIndex=iBase.Index(this,navItem);//获取当前点击在当前区块点击项中的索引
			//切换展开关闭图标
			icoItem[elemIndex].innerHTML= icoItem[elemIndex].innerHTML=='-' ? '+' : '-';
			if(iBase.AttrStyle(boxItem[elemIndex],'display')=='block'){
				//控制项展开状态下,隐藏当前,展开其他的第一项
				//此处有个展开0/1的判断,因为当点击第一个时是不能再展开第一个的
				boxItem[elemIndex].style.display='none';
				if(elemIndex==0){
					boxItem[1].style.display='block';
					icoItem[1].innerHTML='-'
				}else{
					boxItem[0].style.display='block'
					icoItem[0].innerHTML='-'
				}
			}else{
				//控制项展开状态下,展开当前,隐藏其他项
				boxItem[elemIndex].style.display='block';
				for(var k=0;k < boxItem.length; k++){
					if(k!=elemIndex){
						boxItem[k].style.display='none';
						icoItem[k].innerHTML='+';
					}
				}
			}
		}
	}

}

//jQuery版本 ***** start
$(function(){
	//变量定义区
	var _listBox=$('.jq');
	var _navItem=$('.jq>h2');
	var _boxItem=null, _icoItem=null, _parents=null, _index=null;

	//初始化第一个展开
	_listBox.each(function(i){
		$(this).find('div.box').eq(0).show();
		$(this).find('h2>span').eq(0).text('-');
	});

	//遍历所有的点击项
	_navItem.each(function(i){
		$(this).click(function(){
			//找到当前点击父元素为listbox(单个区块)的元素
			_parents=$(this).parents('.listbox');
			_navItem=_parents.find('h2');//此区块中的点击项
			_icoItem=_parents.find('span');//此区块中的展开关闭图标
			_boxItem=_parents.find('div.box');//此区块中展开关闭项
			_index=_navItem.index(this);//取得当前点击在当前区块下点击项中的索引值
			if(_boxItem.eq(_index).is(':visible')){
				//若当前点击项下的展开关闭项是显示的,则关闭,同时展开另外项中的第一个
				_boxItem.eq(_index).hide().end().not(':eq('+_index+')').first().show();
				_icoItem.eq(_index).text('+').end().not(':eq('+_index+')').first().text('-');
			}else{
				//若当前点击项下的展开关闭项是隐藏的,则展开,同时隐藏其他项
				_boxItem.eq(_index).show().end().not(':eq('+_index+')').hide();
				_icoItem.eq(_index).text('-').end().not(':eq('+_index+')').text('+');
			}
		});
	});
});

原生JS实现按数据源均分时间点幻灯效果(已封装)

腾讯新闻详情页有一个事件进展效果, 觉得挺有意思. 于是, 就有了本文的效果: 按数据源均分时间点幻灯. 花了三个多小时写的, 当然, 包括样式与调试. 兼容主流, 建议在Chrom,Firefox,Opera,Safari等标准浏览器中查看. IE下没有阴影及圆角.
实现了根据源数据(样例中是一个JSON数据组)总条数, 均分出时间点以平滑向右动画方式显示在时间线上, 当鼠标划过时间点时, 显示对应的日期及标题. 鼠标划过事件, 充分考虑了用户体验, 当用户快速(无意识移动)从时间点上划过时, 并不触发相应事件.
相关的方法说明及使用, 请参见下面的注释或发评论, 同时也欢迎大家找bug并提交.
原生JS实现按数据源均分时间点幻灯效果
JS核心代码点此查看样例

var JSONData=[{...},{...},...];//数据源,一切皆因它而生,因它而灭

function iTimePoint(iTimeSlideId, dateId, timeLineId, titleTop, titleId, defaultShow){
    /* 传入参数说明:
     * iTimeSlideId: 外围ID名. 本样例DOM中#itimeslide;
     * dateId: 日期ID名. 本样例DOM中#date;
     * timeLineId: 时间点分布ID名. 本样例DOM中#timeline;
     * titleTop: 标题容器上方小三角ID名. 本样例DOM中#titletop;
     * titleId: 标题容器ID名. 本样例DOM中#title;
     * defaultShow: 设定初始显示的时间点, 默认为0, 可不传值
     */

    //参数判断,测试用,成功运行后可删除
    if (arguments.length < 5 || arguments.length>6) {
        alert('参数传入错误,请传入5或6个值! :)');
        return false;
    }

	//通用方法
    var iBase = {
        //document.getElementById
        Id: function(name){
            return document.getElementById(name);
        },
        //时间点动画显示
        PointSlide: function(elem, val){
            //可通过修改i+=5中的5控制滑动速度
            for (var i = 0; i <= 100; i += 5) {
                (function(){
                    //这个pos定义很重要,若直接使用闭包获取到的不是上面的i
                    var pos = i;
                    //平滑移动
                    setTimeout(function(){
                        elem.style.left = pos * val / 100 + 'px';
                    }, (pos + 1) * 10);
                })();
            }
        },
        //为元素添加样式
        AddClass: function(elem, val){
            //若元素无class,直接赋值
            if (!elem.className) {
                elem.className = val;
            }else {
                //否则通过添加空格新增一个class
                var oVal = elem.className;
                oVal += ' ';
                oVal += val;
                elem.className = val;
            }
        },
        //获取元素索引
        Index: function(cur, obj){
            for (var i = 0; i < obj.length; i++) {
                if (obj[i] == cur) {
                    return i;
                }
            }
        }
    }
	//整个函数变量定义区
    var dataLen = JSONData.length;
    var iTimeSilde = iBase.Id(iTimeSlideId);
    var date = iBase.Id(dateId);
    var timeLine = iBase.Id(timeLineId);
    var titletop = iBase.Id(titleTop);
    var title = iBase.Id(titleId);
    var iTimeSildeW = iTimeSilde.offsetWidth;//幻灯区实际宽度
    var timePoint = document.createElement('ul');//用来存储时间点的ul
    var timePointLeft = null;//时间点相对于父元素左边距离
    var timePointLeftCur = null;//每两个时间点间距
    var pointIndex = 0;//时间点在队列中的索引值
	var defaultShow = defaultShow || 0;//默认显示的时间
	var clearFun=null;//当用户无意识的划过时中止执行
	var that=null;
    //根据数据条数生成对应的时间点html
    for (var i = 0; i < dataLen; i++) {
        timePoint.innerHTML += '<li></li>';
    }
    //将时间点插入到时间线DIV中
    timeLine.appendChild(timePoint)
    var timePoints = timeLine.getElementsByTagName('li');
    //时间点平滑显示
    for (var i = 0; i < timePoints.length; i++) {
		//每两个时间点间间距
        timePointLeftCur = parseInt(iTimeSildeW / (dataLen + 1));
		//计算对应时间点左边距
        timePointLeft = (i + 1) * timePointLeftCur;
		//时间点动画形式初始化
        iBase.PointSlide(timePoints[i], timePointLeft);
		//初始显示时间点
        setTimeout(function(){
            timePoints[defaultShow].onmouseover();
        }, 1200);
		//获取时间点默认class值,为鼠标事件做准备
        timePoints[i].oldClassName = timePoints[i].className;
        timePoints[i].onmouseover = function(){
			that = this;//确保clearFun中的this是当前的this
			//提升用户体验,当用户无意识地划过时不执行函数
			clearFun=setTimeout(function(){
				//计算出当前时间点索引值,为鼠标划出做准备
		        pointIndex = iBase.Index(that, timePoints);
				//去除上一个时间点高亮样式
				for (var m = 0; m < timePoints.length; m++) {
					if (m != pointIndex) {
						timePoints[m].className = timePoints[m].oldClassName
					}
				}
				//为当前时间点加载高亮样式
	            iBase.AddClass(that, 'hover');
				//切换日期及标题值
	            date.innerHTML = '<span>' + (JSONData[pointIndex]['date'] || '') + '</span>';
	            title.innerHTML = '<a href="' + (JSONData[pointIndex]['href'] || '') + '">' + (JSONData[pointIndex]['title'] || '') + '</a>';
	            //改变日期及标题的位置,此处减去的数字,可根据实际样式调整
	            date.style.left = ((pointIndex + 1) * timePointLeftCur - 25) + 'px';
	            titletop.style.left = ((pointIndex + 1) * timePointLeftCur + 6) + 'px';
	            //当标题框左边距与标题框宽度之和大于外围宽度时,以右边为绝对点
	            if ((title.offsetWidth + (pointIndex + 1) * timePointLeftCur) < iTimeSildeW) {
	                title.style.left = ((pointIndex + 1) * timePointLeftCur - timePointLeftCur) + 'px';
	            }else {
	                title.style.left = (iTimeSildeW - title.offsetWidth) + 'px';
	            }
	            //显示日期/时间点/标题
	            date.style.display = 'block';
	            titletop.style.display = 'block';
	            title.style.display = 'block';
			},200);//200为认定无意识划过的时间,可自行调节
        }
        timePoints[i].onmouseout = function(){
			//若停留时间低于200ms,认定为无意识划过,中止函数
			clearTimeout(clearFun);
        }
    }
}