【JavaScript从入门到精通】第十七课 JS运动应用-01

此内容来自开课吧石川微信公众号

前几节课我们简单地介绍了运动的基础知识,这节课开始我们重点关注JS运动的具体应用。

多物体运动框架

一个页面可能存在多个物体同时运动的情况,我们要怎么去解决各个定时器之间的干扰呢?我们来看一个简单的例子,下面是我们通过之前所学的知识可以写出来的一个运动效果:

<html>
<head>
    <meta charset="utf-8">
    <title>无标题文档</title>
</head>
<body>
    <div></div>
    <div></div>
    <div></div>
</body>
</html>
div {
    width: 100px;
    height: 50px;
    background: red;
    margin: 10px;
}
window.onload = function () {
    var aDiv = document.getElementsByTagName('div');
    for (var i = 0; i < aDiv.length; i++) {
        aDiv[i].onmouseover = function () {
            startMove(this, 400);
        };
        aDiv[i].onmouseout = function () {
            startMove(this, 100);
        };
    }
};
var timer = null;

function startMove(obj, iTarget) {
    clearInterval(timer);
    timer = setInterval(function () {
        var speed = (iTarget - obj.offsetWidth) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
        if (obj.offsetWidth == iTarget) {
            clearInterval(timer);
        }
        else {
            obj.style.width = obj.offsetWidth + speed + 'px';
        }
    }, 30);
}

效果如下:

【JavaScript从入门到精通】第十七课 JS运动应用-01

可以看到我们希望实现三个div块的缓冲运动,但由于它们使用的是同一个定时器,每次运行startMove函数的时候,上一个定时器都已经被关闭,因此存在互相干扰的问题。为了实现多个元素互不干扰的运动,我们选择使用多个定时器。这里我们采用for循环为每个div的timer初始属性都设置为null,在startMove函数内通过obj.timer的方式区分使用的是哪一个定时器。

<html>
<head>
    <meta charset="utf-8">
    <title>无标题文档</title>
</head>
<body>
    <div></div>
    <div></div>
    <div></div>
</body>
</html>
div {
    width: 100px;
    height: 50px;
    background: red;
    margin: 10px;
}
window.onload = function () {
    var aDiv = document.getElementsByTagName('div');

    for (var i = 0; i < aDiv.length; i++) {
        aDiv[i].timer = null;

        aDiv[i].onmouseover = function () {
            startMove(this, 400);
        };

        aDiv[i].onmouseout = function () {
            startMove(this, 100);
        };
    }
};

//var timer=null;
function startMove(obj, iTarget) {
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var speed = (iTarget - obj.offsetWidth) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
        if (obj.offsetWidth == iTarget) {
            clearInterval(obj.timer);
        }
        else {
            obj.style.width = obj.offsetWidth + speed + 'px';
        }
    }, 30);
}

效果如下:

【JavaScript从入门到精通】第十七课 JS运动应用-01

总的来说,当我们希望页面多个物体同时运动的时候,给每个物体都赋予一个单独的定时器就可以了。现在,我们就拥有了一套多物体运动框架(startMove函数),它将定时器作为了物体的属性,传递的参数为物体本身和目标值。接着,我们再来介绍一个多物体淡入淡出的例子。

<html>
<head>
    <meta charset="utf-8">
    <title>无标题文档</title>
</head>
<body>
    <div></div>
    <div></div>
    <div></div>
</body>
</html>
div {
    width: 200px;
    height: 200px;
    margin: 20px;
    float: left;
    background: red;
    filter: alpha(opacity:30);
    opacity: 0.3;
}
window.onload = function () {
    var aDiv = document.getElementsByTagName('div');
    for (var i = 0; i < aDiv.length; i++) {
        aDiv[i].onmouseover = function () {
            startMove(this, 100);
        };
        aDiv[i].onmouseout = function () {
            startMove(this, 30);
        };
    }
};
var alpha = 30;

function startMove(obj, iTarget) {
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var speed = (iTarget - alpha) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);

        if (alpha == iTarget) {
            clearInterval(obj.timer);
        }
        else {
            alpha += speed;
            obj.style.filter = 'alpha(opacity:' + alpha + ')';
            obj.style.opacity = alpha / 100;
        }
    }, 30);
}

实际上,浏览器并不能完美运行这个程序。可是我们的定时器明明已经拆开了,为什么会出现这样的问题?这个程序的问题在于var alpha=30这一行,我们将alpha写为了全局变量,也就是说它是几个元素的公用变量,因此可能会产生一些矛盾。我们给出的建议是,在多物体框架里,尽可能不要出现公用的元素,否则很容易出现互相干扰的问题。当然,解决这个问题的方法非常简单,我们将alpha作为一个属性加给元素就可以了:

window.onload=function (){
    var aDiv=document.getElementsByTagName('div');
    for(var i=0;i<aDiv.length;i++)
    {
        aDiv[i].alpha=30;
        aDiv[i].onmouseover=function ()
        {
            startMove(this, 100);
        };
        aDiv[i].onmouseout=function ()
        {
            startMove(this, 30);
        };
    }};function startMove(obj, iTarget){
    clearInterval(obj.timer);
    obj.timer=setInterval(function (){
        var speed=(iTarget-obj.alpha)/6;
        speed=speed>0?Math.ceil(speed):Math.floor(speed);
        if(obj.alpha==iTarget)
        {
            clearInterval(obj.timer);
        }
        else
        {
            obj.alpha+=speed;
            obj.style.filter='alpha(opacity:'+obj.alpha+')';
            obj.style.opacity=obj.alpha/100;
        }
    }, 
30);
}

效果如下:

【JavaScript从入门到精通】第十七课 JS运动应用-01

任意值运动框架

现在我们来完善我们的运动框架,让它的功能变得更加强大。例如,我们在页面加入两个div,希望它们具有不同的运动效果:一个变宽,一个变高。

<html>
<head>
    <meta charset="utf-8">
    <title>无标题文档</title>
</head>
<body>
<div id="div1">
    变高
</div>
<div id="div2">
    变宽
</div>
</body>
</html>
div {
    width: 200px;
    height: 200px;
    margin: 20px;
    float: left;
    background: yellow;
}
window.onload = function () {
    var oDiv1 = document.getElementById('div1');
    oDiv1.onmouseover = function () {
        startMove(this, 400);
    };
    oDiv1.onmouseout = function () {
        startMove(this, 200);
    };
    var oDiv2 = document.getElementById('div2');
    oDiv2.onmouseover = function () {
        startMove2(this, 400);
    };
    oDiv2.onmouseout = function () {
        startMove2(this, 200);
    };
};

function startMove(obj, iTarget) {
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var speed = (iTarget - obj.offsetHeight) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
        if (obj.offsetHeight == iTarget) {
            clearInterval(obj.timer);
        }
        else {
            obj.style.height = obj.offsetHeight + speed + 'px';
        }
    }, 30);
}

function startMove2(obj, iTarget) {
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var speed = (iTarget - obj.offsetWidth) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
        if (obj.offsetWidth == iTarget) {
            clearInterval(obj.timer);
        }
        else {
            obj.style.width = obj.offsetWidth + speed + 'px';
        }
    }, 30);
}

这里我们采取了一个比较笨重的方法:使用了2个函数,也就是2套运动框架来完成这个程序。当我们需要完成多个不同的运动时,采用这种写法未免太过复杂。因此,我们应该想方法将它们进行合并。在合并之前,我们先来看一个关于offset的小问题:

<html>
<head>
    <meta charset="utf-8">
    <title>标题文档</title>
</head>
<body>
<div id="div1"></div>
</body>
</html>
#div1 {
    width: 200px;
    height: 200px;
    background: red;
    border: 1px solid black;
}
setInterval(function () {
    var oDiv = document.getElementById('div1');

    oDiv.style.width = oDiv.offsetWidth - 1 + 'px';
}, 30);

效果如下:

【JavaScript从入门到精通】第十七课 JS运动应用-01

可以看到,我们的程序试图制作一个将div1变窄的运动,但实际上div1的运动是变宽了。造成这种情况的原因是我们为div1添加了border属性。对于offsetWidth属性来说,它获得的并不是物体真实的宽度,而是物体的宽度加上它的边框和padding,也就是它的盒模型尺寸。例如一个200px的div块拥有1px的边框和10px的padding,那么它的offsetWidth属性是222。

所以在这个运动里,这个div块一开始的offsetWidth属性其实为202,如果我们将因此算出来第一次定时器运行后div1的width会变为202-1=201px,相当于变宽了,如此反复执行div块就会变宽了。当然,我们如果将-1改为-2的话,这个div块将不会发生任何运动。为了解决这个问题,我们可以将div1的样式写到行间,然后用style获取div1的宽度,但这样限制比较大。另外,还记得我们前面讲过的另一种获取样式的方法:currentStyle/getComputedStyle。

function getStyle(obj, name) {
    if (obj.currentStyle) {
        return obj.currentStyle[name];
    }
    else {
        return getComputedStyle(obj, false)[name];
    }
}

setInterval(function () {
    var oDiv = document.getElementById('div1');
    oDiv.style.width = parseInt(getStyle(oDiv, 'width')) - 1 + 'px';
}, 30);

效果如下:

【JavaScript从入门到精通】第十七课 JS运动应用-01

这样就可以避免offset存在的一个bug了。实际上只要使用offsetWidth等属性都会存在这样的潜在问题,例如我们这节课刚开始的多个div变宽的例子,如果我们给三个div加上边框,那么div块的运动也会发生问题。因此在以后的代码编写中,我们应该尽量选择使用getStyle的这种方法而少去使用offset。

那么回到我们的多物体运动上来,我们用getStyle方法来替代了所有使用offset的方法,可以看到的是,我们使用了非常多的getStyle方法,为了代码的简化,我们可以使用一个变量来储存它。接着,我们观察变高和变宽的代码,它们之间的主要区别在于修改的属性不同,如果我们将这个属性提取出来作为一个参数,那么函数就可以进行合并了:

<html>
<head>
    <meta charset="utf-8">
    <title>无标题文档 </title>
</head>
<body>
<div id="div1">
    变高
</div>
<div id="div2">
    变宽
</div>
<div id="div3">
    safasfasd
</div>
<div id="div4"></div>
</body>
</html>
div {
    width: 200px;
    height: 200px;
    margin: 20px;
    float: left;
    background: yellow;
    border: 10px solid black;
    font-size: 14px;
}
window.onload = function () {
    var oDiv1 = document.getElementById('div1');
    oDiv1.onmouseover = function () {
        startMove(this, 'height', 400);
    };
    oDiv1.onmouseout = function () {
        startMove(this, 'height', 200);
    };
    var oDiv2 = document.getElementById('div2');
    oDiv2.onmouseover = function () {
        startMove(this, 'width', 400);
    };
    oDiv2.onmouseout = function () {
        startMove(this, 'width', 200);
    };
    var oDiv3 = document.getElementById('div3');
    oDiv3.onmouseover = function () {
        startMove(this, 'fontSize', 50);
    };
    oDiv3.onmouseout = function () {
        startMove(this, 'fontSize', 14);
    };
    var oDiv4 = document.getElementById('div4');
    oDiv4.onmouseover = function () {
        startMove(this, 'borderWidth', 100);
    };
    oDiv4.onmouseout = function () {
        startMove(this, 'borderWidth', 10);
    };
};

function getStyle(obj, name) {
    if (obj.currentStyle) {
        return obj.currentStyle[name];
    }
    else {
        return getComputedStyle(obj, false)[name];
    }
}

function startMove(obj, attr, iTarget) {
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var cur = parseInt(getStyle(obj, attr));
        var speed = (iTarget - cur) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
        if (cur == iTarget) {
            clearInterval(obj.timer);
        }
        else {
            obj.style[attr] = cur + speed + 'px';
        }
    }, 30);
}

可以看到,无论是宽度,高度,还是我们没考虑过的字体大小,边框宽度,都可以通过这个运动框架实现运动。不过这个框架在透明度的运动上会出现一些问题,因为我们在获取style的时候采用了parseInt取整,而css透明度的属性为小数,而且我们在设置属性的时候给属性添加了px而透明度属性并没有单位,因此我们需要修改两个地方:一个是样式的获取,一个是样式的设置。

为此,我们需要进行两个相同的判断:获取样式和设置样式时,如果传入的属性参数为opacity(透明度),那么就进行单独处理,如果不是,则走正常流程。

window.onload = function () {
    var oDiv1 = document.getElementById('div1');

    oDiv1.onmouseover = function () {
        startMove(this, 'opacity', 100);
    };
    oDiv1.onmouseout = function () {
        startMove(this, 'opacity', 30);
    };
};

function getStyle(obj, name) {
    if (obj.currentStyle) {
        return obj.currentStyle[name];
    }
    else {
        return getComputedStyle(obj, false)[name];
    }
}

function startMove(obj, attr, iTarget) {
    clearInterval(obj.timer);
    obj.timer = setInterval(function () {
        var cur = 0;
        if (attr == 'opacity') {
            cur = parseFloat(getStyle(obj, attr)) * 100;
        }
        else {
            cur = parseInt(getStyle(obj, attr));
        }
        var speed = (iTarget - cur) / 6;
        speed = speed > 0 ? Math.ceil(speed) : Math.floor(speed);
        if (cur == iTarget) {
            clearInterval(obj.timer);
        }
        else {
            if (attr == 'opacity') {
                obj.style.filter = 'alpha(opcity:' + (cur + speed) + ')';
                obj.style.opacity = (cur + speed) / 100;
            }
            else {
                obj.style[attr] = cur + speed + 'px';
            }
        }
    }, 30);
}

这样,我们的程序既可以用于普通样式的运动,也可以用于透明度的运动了了。那么,这个框架已经完美了吗?

并不是的,如果我们将这个div框的opacity属性输出出来,并在IE7下运行,会发现当鼠标移出时其opacity值并不是0.3而是在0.3左右徘徊。为什么会发生这个问题呢?在IE7下,我们来做一个小测试:

alert(0.07*100);

效果如下:

【JavaScript从入门到精通】第十七课 JS运动应用-01

令人惊讶的是,答案并不是7而比7略大。实际上,对于计算机来说,它并不能真正地去存储一个小数,因为其存储容量是有限的。它存储的,是一个小数的近似值,因此会产生误差。同样地,对于我们写的这个运动框架来说,因为有小数的存在,产生误差是理所当然的事。为了避免这种情况我们需要用到Math对象里的又一个方法:round()方法,它的作用是四舍五入取整:

alert(Math.round(3.4));
alert(Math.round(3.9));
alert(Math.round(3.49999));

结果分别为3,4,3。因此,我们在获取透明度属性的时候,使用round方法对其进行取整,舍去小数部分,就可以解决这个问题了。

到现在为止,我们的运动框架就已经比较完整了,但还远远没有结束,下节课我们将继续对其进行补充。

如需转载,烦请注明出处:https://www.qdskill.com/javascript/548.html

发表评论

登录后才能评论