原生JavaScript运动功能系列(三):多物体多值运动

时间:2021-08-08 16:23:57
  • 多物体同时出发运动函数实现
  • 多属性同步运动变化实现

一、多物同时触发运动函数实现

前面两个动画示例基本理解了动画的核心:位置变化和速度变化,操作的核心就是定时器分段叠加属性值。但是动画还是基于单个元素实现,如果将前面封装的动画实现方法同时触发我可以肯定的告诉你会有bug,我们先来写一个示例看看这个bug是什么?示例需求是有三个div同时居于浏览器左侧,当鼠标进入div时,当前div的宽度变宽,鼠标离开时,宽度也已动画状态恢复。

//html
<div></div>
<div></div>
<div></div> //css
div{
width: 100px;
height: 50px;
margin-bottom: 10px;
background-color: red;
border: 1px solid #000;
}

还是基于昨天的缓冲运动来做,只是将变化样式从相对浏览器位置换成元素宽度,另外因为样式变化,考虑后期也会经常需要获取不同的样式需要,封装了一个获取样式的方法getStyle();这个段代码是为了引出从单个触发动画到多个触发动画函数产生的bug,代码存在bug,也是为后面的内容做铺垫。

 var divArr = document.getElementsByTagName("div");
var timer = null;
for(var i = 0; i < divArr.length; i++){
divArr[i].onmouseover = function(){
startMove(this,400,7);
}
divArr[i].onmouseout = function(){
startMove(this,100,7);
}
}
function getStyle(dom,attr){
if(dom.currentStyle){ return dom.currentStyle[attr];
}else{
return window.getComputedStyle(dom,false)[attr];
}
}
function startMove(dom,target,divisor){
clearInterval(timer);
var iSpeed,iCur;
timer = setInterval(function (){
iCur = parseInt(getStyle(dom,"width"));
iSpeed = (target-iCur)/divisor;
//console.log(iSpeed+"..."+Math.ceil(iSpeed));
iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
if(iCur === target){
clearInterval(timer);
}else{
dom.style.width=iCur+iSpeed+'px';
}
},30);
}

这段代码出现的问题就是,当触发一个div的动画效果,动画还没有运行到终点时,又触发了另一个div的动画,这时前一个动画就会停在当前位置。就像下面这个效果(测试bug效果)。

原生JavaScript运动功能系列(三):多物体多值运动

只有最后一个元素运动到了终点,其实这个bug很明显,从单个元素运动到多个元素运动,首先要看是单点触发动画,还是多点触发动画,上面这个示例就是多点触发动画,多点触发动画就必然需要每个触发点都有自己的一个定时器来执行属于自己的动画程序。那单点触发动画会出现在什么情况呢?开篇的任务列表中有一个链式运动,就是一个触发点接着就是一个元素接着一个元素运动,这种情况就可以使用单点触发,但是为了运动函数的扩展性,一般都采用多点触发封装,在实际开发中,动画函数肯定是被多点触发的。

实质上这个bug是由闭包产生的,因为在上面的运动函数中的定时器,是由写在动画函数执行前的timer变量统一管理,当另一个动画开始执行时,会通过作用域链上的timer将之前的定时器关闭,并在这个变量上赋值自己的定时器来执行自己的动画,解决这个问题的方法很简单就是给每个执行动画拥有自己的timer来单独管理自己的定时器,就不会出现闭包冲突bug了。这个修改很简单,就是给每个触发运动的DOM对象添加一个timer属性来管理自己的定时器就可以了。修改方案如下:

//20、22、28的timer修改成dom.timer
//第二行的var timer = null;删除

二、多属性同步运动变化实现

在前面的所有示例中,我们都是在操作单一样式运动,怎么操作多个样式同步运动变化呢?其实在上一个示例中,我已经埋下了伏笔,这个伏笔就是获取对象样式的方法getStyle(dom,attr)。其实理论上很简单,我们只需要将之前传入的样式终点参数,改成多个样式的终点参数对象就可以了,然后在每次定时器执行时,循环这个对象然后对每个样式进行修改就可以了,这里不讨论js单线程,每个样式修改都是存在先后顺序的,因为计算机的执行速度是以毫秒级速度执行,人眼识别可以忽略这种先后顺序。由于这个功能从功能逻辑上来讲很简单,但是实际处理过程中还是会有些需要注意的细节,而且这些细节有时候会对你的程序造成致命的打击,在没有实现代码之前不太容易用语言表达清楚,所以先上代码,然后再来就着代码剖析具体问题:

 //css
div{
position: absolute;
left: 0;
width: 100px;
height: 50px;
margin-bottom: 10px;
background-color: red;
border: 1px solid #000;
}
//html
<div class="demo"></div> //js
var demoDiv = document.getElementsByClassName("demo")[0];
var targetObj = {
width: 400,
height: 300,
opacity: 50,
left: 150,
top: 150
}
demoDiv.onclick = function(){
startMove(this,targetObj,7);
}
function getStyle(dom,attr){
if(dom.currentStyle){
return dom.currentStyle[attr];
}else{
return window.getComputedStyle(dom,false)[attr];
}
}
function startMove(dom,json,divisor){
clearInterval(dom.timer);
var iSpeed,iCur;
dom.timer = setInterval(function (){
var bStop = true;
for(var attr in json){
if(attr == "opacity"){
iCur = Math.round( parseFloat( getStyle(dom,attr) ) * 100);
}else{
iCur = parseInt( getStyle(dom,attr) );
}
iSpeed = (json[attr]-iCur)/divisor;
iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
if(attr == "opacity"){
dom.style.opacity = (iCur + iSpeed)/100;
}else{
dom.style[attr] = iCur+iSpeed+'px';
}
if( iCur != json[attr]){
bStop = false;
}
}
if(bStop){
clearInterval(dom.timer);
}
},30);
}

一开始看到这个方法估计会有点懵,大神略过,下面我就来将这个方法的全部内容逐个剖析:

  • 39~43:获取变量==>整数值变量与浮点数变量的操作差异;
  • 44~45:计算单次运动距离;
  • 46~50:修改元素样式值;
  • 51~53:判断元素的所有样式是否都到达终点;
  • 55~57:确认样式都到达终点后,结束定时器

需要重点剖析的是获取变量和变量赋值,以及什么时候结束定时器的实际业务逻辑问题。由于opacity的值是0~1,所以获取样式后转成浮点数值这个我相信都可以理解,然后放大100倍是因为考虑到需要计算移动距离,并且可以和其他值一样使用同一的逻辑计算(匹配45行代码),那为什么在乘以100的时候需要做四舍五入(Math.round)的操作呢?如果对js的数值标准IEEE_754浮点数计算标准有所了解就能明白,不明白也没关系,我来给大家解释一下,这是因为js的浮点数计算不精确造成的,比如出现(0.58*100!=58),控制台打印结果是:57.99999999999999,还有比如0.1+0.2!=0.3;太深入的原理就不在这里讲了,需要在这个地方使用Math.round四舍五入的原因就是精度丢失造成的,但是这个差值非常的小,所以可以使用四舍五入的方式获得我们想要的精确的数值,如果这个地方不使用,可能会出现死循环。(这里测试IE11可以没问题,为了准确的获取值是有必要的)。

然后什么时候结束定时器执行这个问题是因为我们不知道再调用方法时会要修改多少样式,而因为运动实际上不会同时到达终点,所以需要判断每个样式都到达终点后结束定时器。