JavaScript动画效果(3):链式运动与同时运动实现

作者:像素漫游

时间:2026-10-30

浏览:428

本文深入讲解JavaScript动画框架的进阶应用,重点实现链式运动与同时运动。通过引入回调函数实现效果依次执行,利用JSON对象遍历实现多属性同步变化,并解决多运动终点判断逻辑,最终封装出支持复杂交互的通用运动插件。

文章目录

动画框架扩展:链式与同时运动概述

在前文关于速度动画、透明度动画、多物体运动及任意值变化的基础上,我们在《Javascript动画效果(二)》中初步封装了一个插件雏形。为了提升框架的实用性,本节将对动画效果进行进一步扩展,重点引入两种新的运动模式:链式运动和同时运动。两者的核心区别在于执行时序:链式运动强调顺序执行,即前一个运动结束后立即触发下一个;而同时运动则要求所有指定的运动属性在同一时间段内并行变化。接下来将详细阐述这两种模式的具体实现方案。

链式运动的实现原理与代码

1、链式运动

在前面的案例中,我们已经掌握了如何对任意属性值进行动态变化。现在的问题是,如何在完成一个动画效果后,无缝衔接另一个动画效果?

核心思路:我们能否在函数参数中传入一个回调函数?当当前动画效果执行完毕后,立即调用该回调函数(即下一个动画效果)。

具体实现:在原有的 startMove(obj, attr, iTarget) 函数定义中,增加一个参数 fn,用于接收代表下一个动画的函数。同时,需要修改定时器结束时的逻辑:在定时器回调函数的末尾增加判断 if(fn){fn(); }。当存在回调函数 fn 时,执行该函数;若不存在,则清除定时器。因此,window.onload 中的调用方式也需相应调整,具体代码如下:


window.onload = function() {
  var Li = document.getElementById('li1');
  Li.onmouseover = function() {
    startMove(Li, 'width', 400, function() {
      startMove(Li, 'height', 200, function() {
        startMove(Li, 'opacity', 100);
      });
    });
  };
  Li.onmouseout = function() {
    startMove(Li, 'opacity', 30, function() {
      startMove(Li, 'height', 100, function() {
        startMove(Li, 'width', 200);
      });
    });
  };
};

基于上述逻辑,我们得到了实现链式运动的完整代码示例:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>链式运动</title>
    <style type="text/css">
      body,ul,li{ margin: 0px; padding: 0px; }
      ul,li{ list-style: none; }
      ul li{ width: 200px; height: 100px; background: yellow; margin-bottom: 20px; border: 4px solid #000; filter:alpha(opacity:30); opacity:0.3; }
    </style>
     
  </head>
  <body>
    <ul>
      <li id="li1"></li>
    </ul>
    <script type="text/javascript">
      window.onload = function(){
        var Li = document.getElementById('li1');
        Li.onmouseover = function(){
          startMove(Li,'width',400,function(){
            startMove(Li,'height',200,function(){
              startMove(Li,'opacity',100);
            });
          });
        };
        Li.onmouseout = function(){
          startMove(Li,'opacity',30,function(){
            startMove(Li,'height',100,function(){
              startMove(Li,'width',200);
            });
          });
        };
      };
     
      function startMove(obj,attr,iTarget,fn){
        clearInterval(obj.timer);
        obj.timer = setInterval(function(){
          var icur = 0;
          if(attr == 'opacity'){
            icur = Math.round(parseFloat(getStyle(obj,attr))*100);
          }else{
            icur = parseInt(getStyle(obj,attr));
          }
          var speed = (iTarget - icur)/10;
          speed = speed>0?Math.ceil(speed):Math.floor(speed);
          if(iTarget == icur){
            clearInterval(obj.timer);
            if(fn){
              fn();
            }
          }
          else{
            if(attr == 'opacity'){
              obj.style.filter = 'alpha(opacity:'+(icur+speed)+')';
              obj.style.opacity = (icur+speed)/100;
            }
            else{
              obj.style[attr] = icur+speed+'px';
            }
          }
        },30)
      }   
       
      function getStyle(obj,attr){
        if(obj.currentStyle){
          return obj.currentStyle[attr];
        }
        else{
          return getComputedStyle(obj,false)[attr];
        }
      }
    </script>
  </body>
</html>

同时运动的逻辑误区与修正

2、同时运动

提到同时运动,初学者可能会认为只需在 onmouseover 事件处理程序中连续调用两次不同的 startMove() 函数即可(这是错误的思路)。实际上,这种写法会导致后调用的效果覆盖先调用的效果,最终只观察到最后一个效果。例如,若想同时改变元素的宽度和高度(高度设置在后),实际表现仅为高度变化,宽度并未改变。既然链式运动通过传参实现连续动画,那么同时运动是否也能通过传参解决?

核心思路:我们是否可以利用 JSON 对象来同时定义并改变多个动画属性?

具体实现:将 startMove(obj, attr, iTarget, fn) 函数中的 attr 和 iTarget 合并为一个参数 json。通过 for/in 循环遍历 json 对象中的数据,例如:


var json = {"a":12,"b":21};
for(var attr in json){
  alert(json);//结果分别是:a,b
  alert(json[attr]);//结果分别是:12,21
}

按照此思路,我们需要依次修改前面的代码逻辑:在定时器函数内部增加 for(var attr in json) {...} 循环,并将原有的 iTarget 替换为 json[attr]。同时,修改 window.onload 中的调用方式,使其传入一个包含多个属性的对象:


window.onload = function() {
  var Li = document.getElementById('li1');
  Li.onmouseover = function() {
    startMove(Li, {
      width: 400,
      height: 200,
      opacity: 100
    });
  };
  Li.onmouseout = function() {
    startMove(Li, {
      width: 200,
      height: 100,
      opacity: 30
    });
  };
};

此时,我们似乎已经实现了预期的同时运动效果。然而,实际运行结果与预期仍有差距。例如,将 onmouseover 事件中的 startMove 宽度参数修改为 202 并执行代码后,我们发现最终的 height 并非 200px,opacity 也不是 100,具体表现如图:

这一结果令人困惑,之前的效果明明运行良好。我们需要回到 JS 代码结构中深入分析,最可能出问题的地方在于 startMove 函数内部的逻辑判断:


if(json[attr] == icur) {
  clearInterval(obj.timer);
  if(fn) {
    fn();
  }
}

目前尚不清楚代码逻辑是等待所有运动属性都到达终点才停止定时器,还是只要有一个属性到达终点就立即停止所有运动。通过前文的测试可以得知,实际结果是:只要有一个运动属性到达终点值,所有运动就会立即停止(此时其他运动可能尚未执行完毕)。我们需要调整这一逻辑。

核心思路:引入一个布尔型标志位 flag,初始赋值为 true。在执行 clearInterval(obj.timer) 之前,先进行判断:若 json[attr] != icur,则将 flag 设为 false。在 else 分支执行前,检查 flag 的状态,若 flag 仍为 true,则清除定时器并执行回调函数 fn。这样即可得到同时运动的完整正确代码:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>链式运动</title>
    <style type="text/css">
      body,ul,li{ margin: 0px; padding: 0px; }
      ul,li{ list-style: none; }
      ul li{ width: 200px; height: 100px; background: yellow; margin-bottom: 20px; border: 4px solid #000; filter:alpha(opacity:30); opacity:0.3; }
    </style>
     
  </head>
  <body>
    <ul>
      <li id="li1"></li>
    </ul>
 
    <script type="text/javascript">
      window.onload = function(){
        var Li = document.getElementById('li1');
        Li.onmouseover = function(){
          startMove(Li,{width:202,height:200,opacity:100});
        };
        Li.onmouseout = function(){
          startMove(Li,{width:200,height:100,opacity:30});
        };
      };
       
      function getStyle(obj, attr) {
        if(obj.currentStyle) {
          return obj.currentStyle[attr];
        } else {
          return getComputedStyle(obj, false)[attr];
        }
      }
       
      function startMove(obj, json, fn) {
        //定义标杆
        var flag = true; //假设的
       
        clearInterval(obj.timer);
        obj.timer = setInterval(function() {
          for(var attr in json) {
            var icur = 0;
            if(attr == 'opacity') {
              icur = Math.round(parseFloat(getStyle(obj, attr)) * 100);
            } else {
              icur = parseInt(getStyle(obj, attr));
            }
            var speed = (json[attr] - icur) / 10;
            speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
            if(json[attr] != icur) {
              flag = false;
            }
            if(attr == 'opacity') {//判断是否为opacity
              obj.style.filter = 'alpha(opacity:' + (icur + speed) + ')';
              obj.style.opacity = (icur + speed) / 100;
            } else {
              obj.style[attr] = icur + speed + 'px';
            }
            if(flag){
              clearInterval(obj.timer);
              if(fn){
                fn();
              }
            } 
          }
        }, 30)
      }
    </script>
  </body>
</html>

至此,同时运动的动画效果便得以完美实现。你是否觉得这一过程充满技巧与逻辑之美?

运动插件封装与总结

至此,我们已经完成了一个基础运动插件的封装。下面对核心代码进行简要解释,并将其保存为 foodoir.animate.js 文件,完整代码如下:


/*
 * 简单的运动框架
 * 作者:foodoir
 * 此框架仅作参考!!!
 *
 * 使用方法见博文
 */
 
function getStyle(obj, attr) {
  if(obj.currentStyle) {
    return obj.currentStyle[attr];
  } else {
    return getComputedStyle(obj, false)[attr];
  }
}
 
function startMove(obj, json, fn) {
 
  clearInterval(obj.timer); //清除定时器,避免重复生成多个定时器
  obj.timer = setInterval(function() {
    var flag = true; //假设刚开始时所有运动都已完成
    for(var attr in json) { //遍历json
 
      var icur = null;
      //1.判断类型
      if(attr == 'opacity') {
        icur = Math.round(parseFloat(getStyle(obj, attr)) * 100);
      } else {
        icur = parseInt(getStyle(obj, attr));
      }
      //2.算速度
      var speed = (json[attr] - icur) / 5;
      speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
      //3.检测停止
      if(icur != json[attr]) {
        flag = false;
      }
      if(attr == 'opacity') {
        obj.style.filter = 'alpha(opacity:' + (icur + speed) + ')';
        obj.style.opacity = (icur + speed) / 100;
      } else {
        obj.style[attr] = icur + speed + 'px';
      }
    }
    if(flag) { //当所有运动都完成时,清除定时器
      clearInterval(obj.timer);
      if(fn) {
        fn();
      }
    }
  }, 30);
}

在后续章节中,我们将介绍如何基于此框架实现多种复杂的动画效果,并将其与 jQuery 中的动画效果进行对比分析。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章