在动画算法与设计的小节中我都把动画一些相关的东西单独提出来分析了,其实 jQuery 的动画的原理就是这样的,这一节我们把设计与算法融合在一起来实现一个伪 jQuery 的动画模块。
在 jQuery 的设计中 ready、ajax、动画都是支持 Promise 这个概念的,也就是通过 Deferred 提供了一个抽象的非阻塞的解决方案,它创建一个 promise 对象,其目的是在未来某个时间点返回一个响应,对应的也就是 done、fail、progress、complete、always等方法,所以在第一部分理解 Deferred 对象是非常重要的,这样才能读懂这类设计。
具体的代码我们可以如右图所示,这里我们开始解释下其设计的目的。
book.animate({ left: '500' }, { duration: 2000 })
就这个动画而言,我们要涉及几个问题,left:500 其实是最终的坐标值。
那么完成这个 left 动画我们至少需要:
每一种属性其实都是有各自的变换的一个算法,包括计算开始值、变化值、时间等等,所以我们必须给每一个变换的属性都进行一个包装,用来封装各自的相关数据,那么我们其实可以用一个类来构造跟属性变化相关的一些信息,如 Tween 类。
通过一个 Tween 类构造出来的缓动对象,其实就是针对每一个属性的封装对象,这样我们只需要设计一个定时器,在指定的时间内调用 Tween 生成的这些对象就可以了,Tween 内部控制着各自属性的状态改变。
具体右边的实现代码涉及了如下几个部分了:
其实动画的整个设计就是这么简单的,代码的复杂是因为还兼容了各种不同情况的处理,比如实现 deferred 机制,针对宽高变化的 overflow 处理,针对 display 为 inline 情况下的处理,那么这些都是一些预处理的机制,在对应的 defaultPrefilter 函数中就能找到,我们没有必要是单独实现了。
其实动画的设计,我们可以学到一个很重要的一点: 封装变化,把每一个属性变化都独立封装一个对象,让其自己管理与控制。
<!doctype html> <html> <head> <meta http-equiv="Content-type" content="text/html; charset=utf-8"/> <style type="text/css"> button{ display: block; } </style> <script src="http://code.jquery.com/jquery-latest.js"></script> </head> <body> <button id="one">jQuery动画的模拟实现:left:50</button> <button id="two">jQuery动画的模拟实现:left:500</button> <ul id="book" alt="" style="background:red;opacity:1;position: relative; left: 300px;width:200px;height:100px;display:inline;"></ul> <script type="text/javascript"> var book = document.getElementById('book'); var $book = $('#book'); var i = 10 while(i){ $book.append("<li>11</li>") i--; } //////////// //创建动画缓动对象 // //////////// function Tween(value, prop, animation) { this.elem = animation.elem; this.prop = prop; this.easing = "swing"; //动画缓动算法 this.options = animation.options; //获取初始值 this.start = this.now = this.get(); //动画最终值 this.end = value; //单位 this.unit = "px" } function getStyles(elem) { return elem.ownerDocument.defaultView.getComputedStyle(elem, null); }; function swing(p) { return 0.5 - Math.cos(p * Math.PI) / 2; } Tween.prototype = { //获取元素的当前属性 get: function() { var computed = getStyles(this.elem); var ret = computed.getPropertyValue(this.prop) || computed[this.prop]; return parseFloat(ret); }, //运行动画 run:function(percent){ var eased //根据缓动算法改变percent this.pos = eased = swing(percent); //获取具体的改变坐标值 this.now = (this.end - this.start) * eased + this.start; //最终改变坐标 this.elem.style[this.prop] = this.now + "px"; return this; } } //////// //动画类 // //////// function Animation(elem, properties, options){ //动画对象 var animation = { elem : elem, props : properties, originalOptions : options, options : options, startTime : Animation.fxNow || createFxNow(),//动画开始时间 tweens : [] //存放每个属性的缓动对象,用于动画 } //生成属性对应的动画算法对象 for (var k in properties) { // tweens保存每一个属性对应的缓动控制对象 animation.tweens.push( new Tween(properties[k], k, animation) ) } //动画状态 var stopped; //动画的定时器调用包装器 var tick = function() { if (stopped) { return false; } //动画时间算法 var currentTime = Animation.fxNow || createFxNow //运动时间递减 remaining = Math.max(0, animation.startTime + animation.options.duration - currentTime), temp = remaining / animation.options.duration || 0, percent = 1 - temp; var index = 0, length = animation.tweens.length; //执行动画改变 for (; index < length; index++) { //percent改变值 animation.tweens[index].run(percent); } //是否继续,还是停止 if (percent <= 1 && length) { return remaining; } else { //停止 return false; } } tick.elem = elem; tick.anim = animation Animation.fx.timer(tick) } //创建开始时间 function createFxNow() { setTimeout(function() { Animation.fxNow = undefined; }); return (Animation.fxNow = Date.now()); } //用于定时器调用 Animation.timers =[] Animation.fx = { //开始动画队列 timer: function(timer) { Animation.timers.push(timer); if (timer()) { //开始执行动画 Animation.fx.start(); } else { Animation.timers.pop(); } }, //开始循环 start: function() { if (!Animation.timerId) { Animation.timerId = setInterval(Animation.fx.tick, 13); } }, //停止循环 stop:function(){ clearInterval(Animation.timerId); Animation.timerId = null; }, //循环的的检测 tick: function() { var timer, i = 0, timers = Animation.timers; Animation.fxNow = Date.now(); for (; i < timers.length; i++) { timer = timers[i]; if (!timer() && timers[i] === timer) { //如果完成了就删除这个动画 timers.splice(i--, 1); } } if (!timers.length) { Animation.fx.stop(); } Animation.fxNow = undefined; } } $("#one").click(function(){ Animation(book, { left: '50' }, { duration: 2000 }) }); $("#two").click(function() { Animation(book, { left: '500' }, { duration: 2000 }) }); </script> </body> </html>