叶落为重生每片落下的叶子都是为了下一次的涅槃...^_^

如jQuery般易用的api风格

    最近几天闲来无事,随便写了点代码玩了玩。个人觉得,应该把编码当做一种乐趣,要不然会觉得很累...

    回到正题,如jQuery般易用的api风格?那到底是什么样的风格呢?个人觉得比较重要的有两点,一是对dom操作的链式调用,并且都呈队列状态,不仅仅使代码的可读语义变得通俗易懂,而且省去了对同一dom元素的多个链式操作时的回调中嵌回调的方式,这是很重要的一点。
    二是对元素的批量操作,这是建立在它强大的选择器上的。jq选择器很强大,这是人所众知的,就不多说了。而且肯定也不是一两天就能实现的了的,所以下面就对我所说的这两点谈谈我的看法。

    基于它强大的选择器,jquery所有的dom操作都依赖于根据它选择器取到的一个数组,很多人喜欢把这叫做jq对象。那咱们暂时也先这样叫吧。然后所有的dom操作都是依赖于这个jq对象中每一个元素并发批量执行的。具体到每一个dom操作,大部分的都是呈链式回调的状态,也就是说在这个方法链里面,直接根据方法调用在链中的先后顺序就能知道他们执行的顺序。这种方法链并且串行的形式是它的一大特色。

    以至于很多人喜欢用jquery,基本就看中它两点,选择器确实很强大,链式调用确实很方便很易用,代码逻辑瞬间变得简单。正因为他把很多的代码逻辑都放到自己内部去处理了,留给编码者考虑的问题就少了很多,所以一方面你觉得好用的同时,也就失去了一次锻炼编码逻辑的机会。因此我不建议初学者直接学习使用jquery或者其他的框架,因为他们会让你对js的理解越来越少。我的观点是所有的框架或者库都是拿来使用的,拿来提高开发效率和管理便利度的,而不是拿来学习的。(当然,研究源码的除外)。

    那么,既然觉得jquery的api风格好用,那我们何尝不尝试一下构建这种类似的api风格呢?(声明:以下尝试都仅仅是提供一种思路,代码并不完善...)
    首先像jquery一样获取一个dom对象的列表。咱们暂时就基于id来获取元素吧。

    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(
thisthis.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 == 1this.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 -*(t/=d)*(t-2) + b;
            },
            easeInOut: 
function(t,b,c,d){
                
if ((t/=d/2< 1return 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< 1return 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 -* ((t=t/d-1)*t*t*t - 1) + b;
            },
            easeInOut: 
function(t,b,c,d){
                
if ((t/=d/2< 1return 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< 1return 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 -* 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(210 * (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==0return b;
                
if (t==d) return b+c;
                
if ((t/=d/2< 1return 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 -* (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< 1return -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==0return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
                if (!|| 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==0return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
                if (!|| 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==0return b;  if ((t/=d/2)==2return b+c;  if (!p) p=d*(.3*1.5);
                
if (!|| a < Math.abs(c)) { a=c; var s=p/4; }
                else var s = p/(2*Math.PI) * Math.asin (c/a);
                
if (t < 1return -.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< 1return 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(
thisthis.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 == 1this.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】

 

<!-- ps -->
因为文件加载的原因,demo2如果一直看到黑屏,看不到背景图的,请刷新或重新运行,或者拷到本地运行...

posted on 2011-01-21 14:34  岑安  阅读(3344)  评论(15编辑  收藏  举报

导航