乐趣区

jQuery之模拟实现animate下

前言:
在上篇的基础上,接入doAnimation()

逻辑图:

实现:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>jQuery 之 $().animate()的实现 </title>
</head>
<body>
<!--<script src="jQuery.js"></script>-->

<div id="A" style="width:100px;height:50px;background-color: deeppink"> 这是 A </div>

<script>
  // (function(a){//   console.log(a) //name
  // })('name')
  //
  // (function (b) {//   console.log(b) //function(){console.log('name')}
  // })(function () {//   console.log('name')
  // })


  // 匿名函数自调用,下面好长好长的 function 就是 $
  // 也就是说 $ 是一个 function(){xxx}
  (function($) {window.$ = $;})(
    // 这里也是匿名函数自调用
    // 本质就是经过一系列操作得到 chenQuery 并作为参数 $,赋值给 window.$
    function() {
      // 匹配 ID
      let rquickExpr = /^(?:#([\w-]*))$/;
      //jQuery 初始化
      function chenQuery(selector) {return new chenQuery.fn.init(selector);
      }

      function getStyles(elem) {return elem.ownerDocument.defaultView.getComputedStyle(elem, null);
      }
      // 模仿 swing 动画效果
      // 两头慢,中间快
      function swing(p) {return 0.5 - Math.cos(p * Math.PI) / 2;
      }

      // 创建动画缓动对象 //
      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"
      }

      Tween.prototype = {
        // 获取元素的当前属性
        get: function() {let computed = getStyles(this.elem);
          let ret = computed.getPropertyValue(this.prop) || computed[this.prop];
          return parseFloat(ret);
        },
        // 运行动画
        run:function(percent){
          let 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 createFxNow() {setTimeout(function() {Animation.fxNow = undefined;});
        return (Animation.fxNow = Date.now());
      }

      let inProgress

      function schedule() {
        //inProgress 是判断整个动画流程是否结束的标志
        // 当 inProgress=null 时,整个动画结束
        if (inProgress) {
          // 走这边
          // 使用 requestAnimationFrame 来完成动画
          // 递归
          window.requestAnimationFrame(schedule);
          /* 执行动画帧 */
          Animation.fx.tick();}

      }

      // 动画核心函数
      function Animation(elem, options, optall,func,){
        // 动画对象
        let animation = {
          elem:elem,
          props:options,
          originalOptions:optall,
          options:optall,
          // 动画开始时间
          startTime:Animation.fxNow || createFxNow(),
          // 存放每个属性的缓动对象,用于动画
          tweens:[]}
        // 生成属性对应的动画算法对象
        for (let k in options) {
          // tweens 保存每一个属性对应的缓动控制对象
          animation.tweens.push(new Tween(options[k], k, animation) )
        }

        // 动画状态
        let stopped;
        // 动画的定时器调用包装器
        // 单帧循环执行
        let tick = function() {if (stopped) {return false;}
          // 动画时间算法
          let currentTime = Animation.fxNow || createFxNow,
            // 动画运动时间递减
            remaining = Math.max(0, animation.startTime + animation.options.duration - currentTime),
            // 百分比
            temp = remaining / animation.options.duration || 0,
            percent = 1 - temp;

          let index = 0,
            length = animation.tweens.length;
          // 执行动画改变
          for (; index < length; index++) {
            //percent 改变值
            animation.tweens[index].run(percent);
          }
          // 当进度不到 100% 时,继续绘制动画帧
          if (percent < 1 && length) {return remaining;}

          // 当结束时通知单个动画结束
          tick.complete()
          return false
        }
        tick.elem = elem;
        tick.anim = animation
        // 这个是自定义的属性,也就是单个动画结束的标志
        tick.complete = func
        // 开始执行下个动画
        Animation.fx.timer(tick)
      }

      // 用于 requestAnimationFrame 调用
      Animation.timers =[]

      Animation.fx = {
        // 开始动画队列,不是帧队列
        //Animation.tick()
        timer: function(timer,) {Animation.timers.push(timer);
          if (timer()) {
            // 开始执行动画
            Animation.fx.start();
            // func()}
          // else {//   Animation.timers.pop();
          // }
        },
        // 开始循环
        start: function(func) {if ( inProgress) {return;}
          // 动画开始即为运行中,加上锁
          inProgress = true;
          // 运行
          schedule();
          // func()},
        // 停止循环
        stop:function(){inProgress = 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;
        }
      }



      // 假设是在数据缓存中存取队列
      const Queue=[]

      // 数据缓存
      const dataPriv={get:function (type) {if(type==='queue') return Queue
        },

      }

      const dequeue=function() {const Queue=dataPriv.get("queue")
        let fn = Queue.shift()
        // 当单个动画结束后,执行下个动画
        const next = function() {dequeue();
        }

        if (fn === "inprogress") {fn = Queue.shift();
        }

        if (fn) {Queue.unshift( "inprogress");
          /* 执行 doAnimation 方法,doAnimation(element, options,function() {firing = false;_fire();})*/
          /*fn 的参数就是形参 func*/
          /*func 方法是用来通知上个动画结束,下个动画运行的重要 function*/
          //func 的作用是用来通知动画执行结束,并继续执行下一个动画
          const func=function() {next();
          }
          fn(func);
        }


      }

      // 省略 type
      const queue=function(element, options, callback,) {
        // 模仿从数据缓存中得到的队列, 直接写 Queue.push 也行
        const Queue=dataPriv.get("queue")
        // 向动画队列中添加 doAnimation 触发器
        Queue.push(function(func) {
          //doAnimation
          callback(element, options, func);
        });
        // 如果没有动画在运行,运行动画
        // 动画锁 inprogress
        if(Queue[0]!=='inprogress'){dequeue()
        }

      }

      /* 动画 */
      const animation = function(element,options) {const doAnimation = function(element, options, func) {
          // const width = options.width

          /*=== 这里面定义了动画的算法,也就是 Animation 实现的地方 ===*/
          // 默认动画时长 2s
          // element.style.transitionDuration = '400ms';
          // element.style.width =  width + 'px';

          /* 监听单个动画完结 */
          //transitionend 事件在 CSS 完成过渡后触发
          // element.addEventListener('transitionend', function() {//   func()
          // });

          // 动画的默认属性
          let optall={complete:function(){},
            old:false,
            duration: 400,
            easing: undefined,
            queue:"fx",
          }

          let anim=Animation(element, options, optall,func)


        }
        // 每调用一次 animation,就入一次队
        return queue(element, options,doAnimation,);
      }


      // 为 chenQuery 的 fn 和 prototype 原型属性 赋 animate 属性
      chenQuery.fn = chenQuery.prototype = {
        // 也可以直接把 animation 里的代码放这里来,但这样就太长了,降低了可读性
        animate: function(options) {animation(this.element, options);
          // 注意返回的是 this, 也就是 $("#A"), 这样就能继续调用 animate 方法
          // 也就是链式调用
          return this;
        }
      }
      // 为 chenQuery 的 fn 属性添加 init 方法
      const init = chenQuery.fn.init = function(selector) {// ["#A", "A",groups: undefined,index: 0,input: "#A"]
        const match = rquickExpr.exec(selector);
        // 这边默认是只找 id 的元素
        const element = document.getElementById(match[1])
        //this 指 chenQuery.fn.init 方法
        // 为该方法添加 element 属性
        this.element = element;
        // 返回 chenQuery.fn.init
        return this;
      }
      // 挺绕的,再将 init 的原型等于 chenQuery.fn 方法
      init.prototype = chenQuery.fn;

      //chenQuery 本身是一个 function(){}
      // chenQuery{
      //init 能调用 fn,fn 能调用 init
      //   fn:{//     animate:function(){},
      //     init:function(){},
      //     // init.prototype=fn
      //   },
      //   prototype:{//     animate:function(){},
      //   }
      // }

      return chenQuery;
    }());


  const A = document.querySelector('#A');
  // 在异步调用中,进行同步调用
  // 动画是异步的
  A.onclick = function() {// 就是连续调用 animation.add()
    $('#A').animate({'width': '500'}).animate({'width': '300'}).animate({'width': '1000'});
  };


</script>
</body>
</html>

运行结果:

解析:
(1)单个动画本身也有循环,也就是利用 requestAnimationFrame 循环动画帧,从而绘制动画
(2)当percent <1 时,即动画运行时间小于总体时间,就不断运行动画帧;当percent =1 时,表示动画结束,通知动画队列,运行下个动画,如此循环即可


(完)

退出移动版