回到正题,如jQuery般易用的api风格?那到底是什么样的风格呢?个人觉得比较重要的有两点,一是对dom操作的链式调用,并且都呈队列状态,不仅仅使代码的可读语义变得通俗易懂,而且省去了对同一dom元素的多个链式操作时的回调中嵌回调的方式,这是很重要的一点。 二是对元素的批量操作,这是建立在它强大的选择器上的。jq选择器很强大,这是人所众知的,就不多说了。而且肯定也不是一两天就能实现的了的,所以下面就对我所说的这两点谈谈我的看法。
基于它强大的选择器,jquery所有的dom操作都依赖于根据它选择器取到的一个数组,很多人喜欢把这叫做jq对象。那咱们暂时也先这样叫吧。然后所有的dom操作都是依赖于这个jq对象中每一个元素并发批量执行的。具体到每一个dom操作,大部分的都是呈链式回调的状态,也就是说在这个方法链里面,直接根据方法调用在链中的先后顺序就能知道他们执行的顺序。这种方法链并且串行的形式是它的一大特色。 以至于很多人喜欢用jquery,基本就看中它两点,选择器确实很强大,链式调用确实很方便很易用,代码逻辑瞬间变得简单。正因为他把很多的代码逻辑都放到自己内部去处理了,留给编码者考虑的问题就少了很多,所以一方面你觉得好用的同时,也就失去了一次锻炼编码逻辑的机会。因此我不建议初学者直接学习使用jquery或者其他的框架,因为他们会让你对js的理解越来越少。我的观点是所有的框架或者库都是拿来使用的,拿来提高开发效率和管理便利度的,而不是拿来学习的。(当然,研究源码的除外)。 那么,既然觉得jquery的api风格好用,那我们何尝不尝试一下构建这种类似的api风格呢?(声明:以下尝试都仅仅是提供一种思路,代码并不完善...)
var get = function (ids) { var d = document, a = -1; this.elements = []; if (typeof ids != 'string' && !!ids.length) { for (var i=0; i<ids.length; i++) { var id = ids[i], o; o = typeof id == 'string' ? d.getElementById(id) : id; this.elements.push(o); } } else { while (typeof arguments[++a] == 'string') { this.elements.push(d.getElementById(arguments[a])); } } }
然后为它扩展一些操作dom的方法
get.prototype = { each : function () {}, animate : function () {} }
当然,这种方式和jQuery看起来不太一样,但能理解就行,jquery可能是这个样子:
jQuery = window.jQuery = window.$ = function( selector, context ) {
return new jQuery.fn.init( selector, context ); }
jQuery.fn = jQuery.prototype = { init: function( selector, context ) {} }
接下来对获取的队列进行批量操作,不可避免的就需要一个each的遍历方法。
each : function (fn) { for (var i=0; i<this.elements.length; i++) { fn.call(this, this.elements[i]) } return this; },
each为get.prototype扩展出的方法,提供一个参数function,并且遍历dom列表,把function绑定到每一个元素上。然后让它返回get.prototype,因为prototype本身具有类似于“超类”的性质,所以凡是返回给prototype对象的方法都能继续调用prototype扩展出来到方法。
为了使这个尝试更有意义一点,接下来来做一个animate的函数吧。这个函数是jquery对dom操作很常用的一个方法,有了它,大部分的动画都变得那么简单和容易了。下面会是一个简单的实现:
animate: function (config) { if (!this.animQueue) this.animQueue = HR._animQueue = []; var a = 0, time, tween, ease, callback; while (arguments[++a]) { if (typeof arguments[a] == 'number') time = arguments[a]; if (typeof arguments[a] == 'string') { if (/^ease*/.test(arguments[a])) ease = arguments[a]; else tween = arguments[a]; } if (HR.isFunction(arguments[a])) callback = arguments[a]; }
this.animQueue.push({ config: config, time: time, tween: tween, ease: ease, callback: callback }); if (this.animQueue.length == 1) this.execute(this.animQueue);
return this; },
光看这一段可能看不出什么端倪,是的,因为要像jquery一样做成串行的方法链,就需要一个临时队列来操作,要不然即使方法链形成了,但这些方法都是并行的,达不到我们想要的效果。所以上面一段代码主要是处理animate推入队列的一个逻辑,然后对参数arguments做了一些判断,以便在写参数的时候能更加随意,除了第一个参数和最后一个callback外,其余参数不用考虑位置和是否必填,以增强易用性。 核心的变换函数在execute上,
execute : function (queue) { var _this = this, m = 0, n = 0, _anim = function (el, key, from, to, at, tw, ease, cb) { var isOP = (key == 'opacity' && !HR.support.opacity), _key = key; if (isOP) {to = to*100; _key = 'filter'} var s = +new Date, d = at, b = parseFloat(from) || 0, c = to-b;
(function () { var t = +new Date - s; if (t >= d) { n ++; t = d; el.style[_key] = (isOP ? 'alpha(opacity=' : '') + Tween.Linear(t, b, c, d) + (key != 'opacity' ? 'px' : '') + (isOP ? ')' : ''); !!cb && cb.apply(el); if (m == n && _this.animQueue.length > 1) { _this.animQueue.shift(); _this.execute(_this.animQueue); }
return; } el.style[_key] = (isOP ? 'alpha(opacity=' : '') + Tween[tw][ease](t, b, c, d) + (key != 'opacity' ? 'px' : '') + (isOP ? ')' : '');
if (!HR.timers[el.id]) HR.timers[el.id] = []; HR.timers[el.id].push(setTimeout(arguments.callee, 16));
})(); }, _q = this.animQueue[0];
return this.each(function (el) { for (var k in _q.config) { m ++; _anim(el, k, k == 'opacity' && !HR.support.opacity ? HR.getStyle('filter', el) == '' ? 100 : parseInt(HR.getStyle('filter', el).match(/\d{1,3}/g)[0]) : HR.getStyle(k, el), _q.config[k], typeof _q.time == 'number' ? _q.time : 1000, typeof _q.tween == 'string' && !/^ease*/.test(_q.tween) ? _q.tween : 'Quart', typeof _q.ease == 'string' && /^ease*/.test(_q.ease) ? _q.ease : 'easeOut', _q.callback) } }); }
这一段看起来就要复杂一些了,最基本的变化还是在_anim这个私有函数上。其余的代码基本在做一些批量的操作,和透明度变化兼容性,以及当前变换是否执行完毕的功能。结合这两段,基本就实现了jquery的animate的效果了。属于一个简化版本。 当然,还不能忘了很重要的一点,就是既然可以变换,那就必须有个stop的方法让这个变换可控,要不然这个代码的可用性会大打折扣,参考以下代码:
stop : function (clearQueue) { if (clearQueue) HR._animQueue.length = 0; this.each(function (el) { if (!!HR.timers[el.id]) for (var i=0; i<HR.timers[el.id].length; i++) clearTimeout(HR.timers[el.id][i]) }); return this; },
针对不同的dom元素id设置专门的临时计时器存贮,HR.timers[el.id],然后遍历当前dom列表,把对应的计时器clear掉。参数clearQueue作为可选参数,用来控制是否清掉后续等待执行的animate。
为了让这个方法更加好玩一点,我加了几种额外的缓动方式,jquery只有一种swing,然后所有的缓动算法放置在Tween对象中以供使用。下面是我做测试的源码,(如有纰漏,各位见谅)
/* =========== animate js ============ */ /* @author:hongru.chen */ /* =================================== */
if (typeof HR == 'undefined' || !HR) HR = { extend : function (destination, source, override) { if (override === #ff0000) override = true; for (var property in source) { if (override || !(property in destination)) { destination[property] = source[property]; } } return destination; } };
(function () {
var Tween = { // 以下算子的参数分别表示: t:运行时间,b:开始量,c:总变化量,d:总时间 Linear: function(t,b,c,d){ return c*t/d + b; }, Quad: { easeIn: function(t,b,c,d){ return c*(t/=d)*t + b; }, easeOut: function(t,b,c,d){ return -c *(t/=d)*(t-2) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t + b; return -c/2 * ((--t)*(t-2) - 1) + b; } }, Cubic: { easeIn: function(t,b,c,d){ return c*(t/=d)*t*t + b; }, easeOut: function(t,b,c,d){ return c*((t=t/d-1)*t*t + 1) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t*t + b; return c/2*((t-=2)*t*t + 2) + b; } }, Quart: { easeIn: function(t,b,c,d){ return c*(t/=d)*t*t*t + b; }, easeOut: function(t,b,c,d){ return -c * ((t=t/d-1)*t*t*t - 1) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t*t*t + b; return -c/2 * ((t-=2)*t*t*t - 2) + b; } }, Quint: { easeIn: function(t,b,c,d){ return c*(t/=d)*t*t*t*t + b; }, easeOut: function(t,b,c,d){ return c*((t=t/d-1)*t*t*t*t + 1) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b; return c/2*((t-=2)*t*t*t*t + 2) + b; } }, Sine: { easeIn: function(t,b,c,d){ return -c * Math.cos(t/d * (Math.PI/2)) + c + b; }, easeOut: function(t,b,c,d){ return c * Math.sin(t/d * (Math.PI/2)) + b; }, easeInOut: function(t,b,c,d){ return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b; } }, Expo: { easeIn: function(t,b,c,d){ return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; }, easeOut: function(t,b,c,d){ return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, easeInOut: function(t,b,c,d){ if (t==0) return b; if (t==d) return b+c; if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; } }, Circ: { easeIn: function(t,b,c,d){ return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b; }, easeOut: function(t,b,c,d){ return c * Math.sqrt(1 - (t=t/d-1)*t) + b; }, easeInOut: function(t,b,c,d){ if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b; return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b; } }, Elastic: { easeIn: function(t,b,c,d,a,p){ if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; if (!a || a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; }, easeOut: function(t,b,c,d,a,p){ if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; if (!a || a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); return (a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b); }, easeInOut: function(t,b,c,d,a,p){ if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5); if (!a || a < Math.abs(c)) { a=c; var s=p/4; } else var s = p/(2*Math.PI) * Math.asin (c/a); if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b; return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b; } }, Back: { easeIn: function(t,b,c,d,s){ if (s == undefined) s = 1.70158; return c*(t/=d)*t*((s+1)*t - s) + b; }, easeOut: function(t,b,c,d,s){ if (s == undefined) s = 1.70158; return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b; }, easeInOut: function(t,b,c,d,s){ if (s == undefined) s = 1.70158; if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b; return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b; } }, Bounce: { easeIn: function(t,b,c,d){ return c - Tween.Bounce.easeOut(d-t, 0, c, d) + b; }, easeOut: function(t,b,c,d){ if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b; } else { return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b; } }, easeInOut: function(t,b,c,d){ if (t < d/2) return Tween.Bounce.easeIn(t*2, 0, c, d) * .5 + b; else return Tween.Bounce.easeOut(t*2-d, 0, c, d) * .5 + c*.5 + b; } } }
var get = function (ids) { var d = document, a = -1; this.elements = []; if (typeof ids != 'string' && !!ids.length) { for (var i=0; i<ids.length; i++) { var id = ids[i], o; o = typeof id == 'string' ? d.getElementById(id) : id; this.elements.push(o); } } else { while (typeof arguments[++a] == 'string') { this.elements.push(d.getElementById(arguments[a])); } } }
get.prototype = {
each : function (fn) { for (var i=0; i<this.elements.length; i++) { fn.call(this, this.elements[i]) } return this; },
setStyle : function (p, v) { this.each(function (el) { el.style[p] = v; }); return this; },
show : function () { var _this = this; this.each(function (el) { _this.setStyle('display', 'block'); }) return this; },
hide : function () { var _this = this; this.each(function (el) { _this.setStyle('display', 'none'); }) return this; },
animate: function (config) { if (!this.animQueue) this.animQueue = HR._animQueue = []; var a = 0, time, tween, ease, callback; while (arguments[++a]) { if (typeof arguments[a] == 'number') time = arguments[a]; if (typeof arguments[a] == 'string') { if (/^ease*/.test(arguments[a])) ease = arguments[a]; else tween = arguments[a]; } if (HR.isFunction(arguments[a])) callback = arguments[a]; }
this.animQueue.push({ config: config, time: time, tween: tween, ease: ease, callback: callback }); if (this.animQueue.length == 1) this.execute(this.animQueue);
return this; },
stop : function (clearQueue) { if (clearQueue) HR._animQueue.length = 0; this.each(function (el) { if (!!HR.timers[el.id]) for (var i=0; i<HR.timers[el.id].length; i++) clearTimeout(HR.timers[el.id][i]) }); return this; },
execute : function (queue) { var _this = this, m = 0, n = 0, _anim = function (el, key, from, to, at, tw, ease, cb) { var isOP = (key == 'opacity' && !HR.support.opacity), _key = key; if (isOP) {to = to*100; _key = 'filter'} var s = +new Date, d = at, b = parseFloat(from) || 0, c = to-b;
(function () { var t = +new Date - s; if (t >= d) { n ++; t = d; el.style[_key] = (isOP ? 'alpha(opacity=' : '') + Tween.Linear(t, b, c, d) + (key != 'opacity' ? 'px' : '') + (isOP ? ')' : ''); !!cb && cb.apply(el); if (m == n && _this.animQueue.length > 1) { _this.animQueue.shift(); _this.execute(_this.animQueue); }
return; } el.style[_key] = (isOP ? 'alpha(opacity=' : '') + Tween[tw][ease](t, b, c, d) + (key != 'opacity' ? 'px' : '') + (isOP ? ')' : '');
if (!HR.timers[el.id]) HR.timers[el.id] = []; HR.timers[el.id].push(setTimeout(arguments.callee, 16));
})(); }, _q = this.animQueue[0];
return this.each(function (el) { for (var k in _q.config) { m ++; _anim(el, k, k == 'opacity' && !HR.support.opacity ? HR.getStyle('filter', el) == '' ? 100 : parseInt(HR.getStyle('filter', el).match(/\d{1,3}/g)[0]) : HR.getStyle(k, el), _q.config[k], typeof _q.time == 'number' ? _q.time : 1000, typeof _q.tween == 'string' && !/^ease*/.test(_q.tween) ? _q.tween : 'Quart', typeof _q.ease == 'string' && /^ease*/.test(_q.ease) ? _q.ease : 'easeOut', _q.callback) } }); } }
HR.extend(HR, { get : function () { return new get(arguments); }, isFunction : function(o) { return typeof(o) == 'function' && (!Function.prototype.call || typeof(o.call) == 'function'); }, getStyle : function (p, el) { return el.currentStyle ? el.currentStyle[p] : document.defaultView.getComputedStyle(el, null).getPropertyValue(p); }, support : (function () { try { var d = document.createElement('div'); d.style['display'] = 'none'; d.innerHTML = '<a style="float:left; opacity:.5;"></a>'; var a = d.getElementsByTagName('a')[0]; return { opacity: a.style.opacity === '0.5' } } finally { d = null; } })(),
timers : {}
}); })();
然后为了让大家看的直观一点,小做了两个demo 【demo1】
【demo2】
|