1.多元素的单一属性变化
首先,从多元素的单一属性变化开始。为方便叙述,以多个 div 的 宽度变化为例子进行说明。
整个思路比较简单,就是对每个 div 都 设置定时器并监听鼠标事件,加上js运动(一)—— sidebar(分享到)中提及的缓动知识即可。
但别看简单,在实际开发中,还是会遇到不少的陷阱,各式陷阱里面也藏着很多知识点。详见下文。
1.1 var 和 let
首先,看一下下面的代码。从 Java 编程的角度来看,似乎没有什么问题。但在实际运行时会发现,不管鼠标进入到哪个 div ,都是最后一个 div 的宽度在变化。这是怎么回事呢?
原来,JavaScript的变量缺少块级作用域的支持。这样,上图中var oDiv = oDivs[i];
这句话就成了罪魁祸首。
针对这个缺陷,JavaScript 增加了 let
关键字。它不是保留关键字,只有 js 1.7 及之后的版本才支持,所以再使用时需要手动加入版本号。
加入版本号的方法:当使用Spidermonkey 或 Rhino 作为单独的解析器时,可以通过命令行指定,或者调用内置函数version()
,传入一个实际版本号*100的数值,如指定 js 1.7版本需传入170;当使用Firefox,可以在script标签中指定。
<script type="application/javascript; version = 1.7">
《JavaScript权威指南》 (P272)对 let 的使用方式总结为:
- 可以作为变量声明,和 var 一样
- 在
for
或者for/in
循环中,作为 var 的替代方案 - 在语句块中定义一个新变量并显示指定它的作用域
- 定义一个在表达式内部作用域中的变量,这个变量只在表达式中可用
所以如果要像Java一样用,就得把 var oDiv = oDivs[i];
改成 let oDiv = oDivs[i];
。或者,我们直接用 oDivs[i]
,不再过渡一下,如下图所示(注意,moveTowards中第一个参数简化成了this)。
1.2 定时器分配
为了每个元素的运动不被其他元素干扰,需要每个元素都分配一个定时器。
在自己做的时候,我把定时器放在了缓动函数 moveTowards
里面(js运动(一)—— sidebar(分享到)中缓动函数名为moveTo,因为js其实自带了一个同名的函数,所以为了不混淆,这里改成了moveTowards) 。如下图。
这种做法引入一个问题:当频繁的进行鼠标移入移出时,可能存在多个定时器作用于同一个元素的情况,会导致整个元素不断来回的抖动。正确的做法为,为每个元素定义一个定时器属性,在关闭定时器或者开启定时器的时候,对应该元素进行操作即可。如下图所示。
所以,关闭定时器就变成了clearInterval(obj.timer);
。
1.3 缓动速度分配
在js运动(一)—— sidebar(分享到)的缓动函数中,speed 定义和赋值如下图。
这么定义适用于单个元素,但放到多个元素的场景时,就会出现下图中的情况。
其实很简单,定时器都分配了,速度当然也得各自分配。所以speed的赋值和处理应该放在定时器里。
综上,多元素单属性变化的小程序如下。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Multi Elt Sp</title>
<style>
div {width:100px; height:50px; background-color:red; margin:10px;}
</style>
<script>
window.onload = function(){
var oDivs = document.getElementsByTagName("div");
for(var i=0; i < oDivs.length; i++){
oDivs[i].timer = null;
oDivs[i].onmouseover = function()
{
moveTowards(this, 400);
}
oDivs[i].onmouseout = function()
{
moveTowards(this, 100);
}
}
}
function moveTowards(obj, iTarget){
var speed;
clearInterval(obj.timer);
obj.timer = setInterval(function(){
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);
}
</script>
</head>
<body>
<div id="div1"></div>
<div id="div2"></div>
<div id="div3"></div>
</body>
</html>
单属性的变化依据上述小程序修改即可,只有一点需要特别注意,即透明度的变化。需要修改缓动函数。
根据《JavaScript权威指南》 (P423),透明度用opacity属性来处理,属性值为0~1之间的数字,1代表100%不透明(默认值),而0代表0%不透明(或100%透明)。opacity属性在当今所有浏览器中都支持,除了IE。IE提供类似的可选方法:IE特有的filter属性。让元素75%不透明,可以使用以下CSS样式:
opacity:.75
filter:alpha(opacity=75)
为了兼容性,不管在定义样式的时候还是在进行运动操作的时候,都需要两个一起写。赋值时如下。
obj.style.filter = 一个整数 //IE
obj.style.opacity = 一个小数 //others
2. offset* 的陷阱、“层叠”及计算样式的获取
2.1 offset* 量中的陷阱
在js运动(一)—— sidebar(分享到)和 js运动(二)—— 右侧悬浮框及前文中都用到了很多 offset* 量,如offsetHeigt
、offsetWidth
等。但从js运动(二)—— 右侧悬浮框中知道,offset* 量是要算上元素的边框大小的(原教程中有一个错误,offsetWidth 里面不包含padding),这使得有时候运动会适得其反。
例如,一个width:200px
且 border:1px solid black
的元素,想让其宽度每次以1px逐一减小,则在定时器里面,
obj.style.width = obj.offsetWidth - 1 + "px";
这看似没什么问题,但在实际运行中会发现,元素的宽度不但没有逐一减少,还逐一增大了,这是怎么回事呢?看看下面的计算。
obj.offsetWidth = obj.style.width + obj.style.border *2 = 202 px;
所以 obj.style.width = 202 -1 = 201 px;
所以 obj.offsetWidth = obj.style.width + obj.style.border *2 = 203 px;
以此类推。
所以可以看到元素的宽度在增大,与想要的结果背道而驰。
这就要说到层叠效果和计算样式了,见下文。
2.2 层叠
根据《JavaScript权威指南》(P412),
CSS中的C即为层叠的意思,该术语指示了应用于文档中任何给定元素的样式规则是各个来源的层叠效果,这些来源为:
- Web浏览器的默认样式表
- 文档的样式表
- 每个独立的HTML元素的style属性
style
属性中的样式覆盖了样式表中的样式,文档的样式表中的样式覆盖了浏览器的默认样式,任意给定元素的视觉表现可能是来自3个来源的一个样式组合。一个元素甚至可能匹配样式表中的多个选择器,在这种情况下,所有这些选择器的关联样式属性都将应用到该元素上。
为显示文档元素,Web浏览器必须组合元素的style属性,包括来自文档样式表中所有匹配的选择器的样式值。计算的结果是一组实际用于显示元素的样式属性和值,这组值就是元素的计算样式。
2.3 计算样式
根据《JavaScript权威指南》(P432),
用浏览器窗口的getComputedStyle()
方法获取一个元素的计算样式,此方法的第一个参数为需要获取样式的元素,第二个参数通常为null或者空字符串,但也是必须的。其用法如下。
window.getComputedStyle(div, null).fontsize
该方法的返回值是一个CSSStyleDeclaration
对象,这个对象的特征为:
- 计算样式的属性是只读的
- 计算样式的值是绝对值,类似百分比和点之类相对的单位将全部转换为绝对值。有指定尺寸的值将会是以 px 为后缀的字符串。颜色将会以
“rgb(#,#,#)”或“rgba(#,#,#,#)”
的格式返回 - 不计算符合样式,如可以查询marginLeft 但不能查询 margin
- 计算样式的cssText属性未定义
这个方法在IE中存在兼容性问题,对于IE,使用元素的currentStyle属性。它不是真正的计算样式,因为没有将相对值转换成绝对值,会返回带相对性单位(如%或em)的尺寸或者非精确的颜色值(如red)。
为了兼容IE和其他浏览器,可以定义一个函数获取元素的计算样式。如下。
function getStyle(obj, name) {
if(obj.currentStyle){
return obj.currentStyle[name];
}else{
return getComputedStyle(obj, false)[name];
}
}
注意,所得结果中带单位,使用时一定要用parseInt
将结果转换成数字。
3. 多元素的运动
现实开发中,一个页面里可能有多个元素在动,而且光怪陆离。这就是接下来要是的多元素的多属性变化。
既然是多元素、多属性,所以元素和属性都要作为参数传入缓动函数。另外,需要注意的是,透明度的缓动和尺寸的缓动还是有所区别的,所以应该区分处理。
function moveTowards(obj, attr, iTarget) {
clearInterval(obj.timer);
obj.timer = setInterval(function (){
var cur = 0;
if(attr == 'opacity'){
cur = Math.round(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(opacity:' + (cur + speed) + ')';
obj.style.opacity = (cur + speed) / 100;
}else{
obj.style[attr] = cur + speed + 'px';
}
}
}, 30);
}
需要注意的地方:
parseFloat
在计算并储存时,可能出现小数点后多位的情况,需要用Math.round
取整;getStyle
所得结果中带单位,使用时一定要用parseInt
将结果转换成数字;- IE与其他浏览器对透明度的定义不一致,需要考虑兼容性。
4.总结
以上即为今天的内容。初初入门,小小程序还是会遇到不少问题,不得不感叹,小程序也可能蕴藏大知识,手跟上脑,才能学会。
参考文献
【1】《JavaScript权威指南》
【2】智能社:JavaScript教程——从入门到精通 ( https://ke.qq.com/course/152997?taid=766917950461349 )
【3】js运动(一)—— sidebar(分享到)( https://blog.csdn.net/qq_31305965/article/details/89676391 )
【4】js运动(二)—— 右侧悬浮框( https://blog.csdn.net/qq_31305965/article/details/89886644 )