如jQuery般易用的api风格代码分享

  回到正题,如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】

  

   [Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]

  【demo2】

  

   [Ctrl+A 全选 注:如需引入外部Js需刷新才能执行]