JavaScript 运动 02 —— animate 函数增强
上文我们实现了匀速运动,本文在上文的基础上进行一些改进,最终目标是:
- 多值同时运动
- 链式运动
- 使用 async/await 和 Promise 解决链式运动回调嵌套问题
多值运动
解决多值运动,我们需要之前的单一属性参数替换为对象字面量,以容纳更多的属性值。
对 animate 函数进行修改:
function animate(ele = null,config = {
speed:10,
attrs:{
}
}){
// 清除定时器
clearInterval(ele.timer);
const attrs = config.attrs;
ele.timer = setInterval(()=>{
for(const attr in attrs){
let speed = config.speed;
// 设置目标距离
let target = Number.parseInt(attrs[attr]);
// 获取当前的样式
let currentStyle = (attr === "opacity")?(Number.parseInt(Number.parseFloat(getCurrentStyle(ele,attr))*100)):Number.parseInt(getCurrentStyle(ele,attr));
// 如果改变的样式是 opacity,target乘以100
if(attr === "opacity"){
target = Number.parseInt(Number.parseFloat(attrs[attr])*100);
}
// 根据当前样式值和目标位置的差值判断速度方向
if(currentStyle - target > 0){
speed = -Math.abs(speed);
}
// 运动到目标点后清除定时器
if(Math.abs(target - currentStyle) < Math.abs(speed)){
ele.style[attr] = (attr === "opacity")? target / 100 : target + "px";
// clearInterval(ele.timer);
}else{
// 根据当前样式动态改变物体的样式
ele.style[attr] = (attr === "opacity")?( currentStyle + speed)/100:(currentStyle + speed) + "px";
currentStyle += speed;
}
}
},30);
}
调用运动函数:
...
const ele = document.getElementById("inner");
const par = document.getElementById("par");
function start(){
animate(ele,{
speed:10,
attrs:{
left:"500px",
top:par.offsetHeight - ele.offsetHeight*0.5 - 2,
opacity:1,
}
});
}
...
效果图:
多值同时运动-1.gif这里我们将 speed 也一并打包放入 config 对象中,已解决参数过多的问题。
可以看到,在此次运动中,三个属性(left,top,opacity)同时进行了变化,适用性更加广泛了。
清除定时器
也许你已经注意到了,上面的 animate 函数中,我们将 clearInterval 清除定时器注释掉了。这是因为在多值运动的时候,并不保证所有的属性都能同时完成,因此当一个运动完成后,如果清除定时器,那么后序没有完成的运动也将一并停止。效果就不演示了,大家可以自己把注释去掉试一试。
可是定时器总是要清除的呀,不然它将会一直在后台浪费资源,那么应该怎么做呢?
定义一个标志变量 flag,用来决定是否关闭定时器,当有运动还没有完成时,flag 为 false,当运动全都完成后,flag 变为 true,同时关闭定时器。
修改 animate 函数:
function animate(ele = null,config = {
speed:10,
attrs:{
}
}){
// 清除定时器
clearInterval(ele.timer);
const attrs = config.attrs;
ele.timer = setInterval(()=>{
let flag = true;
for(const attr in attrs){
let speed = config.speed;
// 设置目标距离
let target = Number.parseInt(attrs[attr]);
// 获取当前的样式
let currentStyle = (attr === "opacity")?(Number.parseInt(Number.parseFloat(getCurrentStyle(ele,attr))*100)):Number.parseInt(getCurrentStyle(ele,attr));
// 如果改变的样式是 opacity,target乘以100
if(attr === "opacity"){
target = Number.parseInt(Number.parseFloat(attrs[attr])*100);
}
// 根据当前样式值和目标位置的差值判断速度方向
if(currentStyle - target > 0){
speed = -Math.abs(speed);
}
// 根据运动是否完成来设定 flag
if(currentStyle !== target){
flag = false;
}
// 运动到目标点后清除定时器
if(Math.abs(target - currentStyle) < Math.abs(speed)){
ele.style[attr] = (attr === "opacity")? target / 100 : target + "px";
}else{
// 根据当前样式动态改变物体的样式
ele.style[attr] = (attr === "opacity")?( currentStyle + speed)/100:(currentStyle + speed) + "px";
currentStyle += speed;
}
}
if(flag){
clearInterval(ele.timer);
}
},30);
}
此时,当多个属性的运动都完成后,定时器就会关闭。
链式运动
链式运动,就是在前一次运动完成后,再进行下一次运动。这样一来,我们只需在前一次运动完成并清除定时器之后,进行下一次运动,我们可以在 config 中定义一个回调函数,用来在运动完成后调用:
修改 animate 函数:
function animate(ele = null,config = {
speed:10,
attrs:{},
done:null,
}){
// 清除定时器
clearInterval(ele.timer);
const attrs = config.attrs;
ele.timer = setInterval(()=>{
let flag = true;
for(const attr in attrs){
let speed = config.speed;
// 设置目标距离
let target = Number.parseInt(attrs[attr]);
// 获取当前的样式
let currentStyle = (attr === "opacity")?(Number.parseInt(Number.parseFloat(getCurrentStyle(ele,attr))*100)):Number.parseInt(getCurrentStyle(ele,attr));
// 如果改变的样式是 opacity,target乘以100
if(attr === "opacity"){
target = Number.parseInt(Number.parseFloat(attrs[attr])*100);
}
// 根据当前样式值和目标位置的差值判断速度方向
if(currentStyle - target > 0){
speed = -speed;
}
// 根据运动是否完成来设定 flag
if(currentStyle !== target){
flag = false;
}
// 运动到目标点后清除定时器
if(Math.abs(target - currentStyle) < Math.abs(speed)){
ele.style[attr] = (attr === "opacity")? target / 100 : target + "px";
}else{
// 根据当前样式动态改变物体的样式
ele.style[attr] = (attr === "opacity")?( currentStyle + speed)/100:(currentStyle + speed) + "px";
currentStyle += speed;
}
}
if(flag){
clearInterval(ele.timer);
config.done && config.done();
}
},30);
}
调用运动函数:
...
const ele = document.getElementById("inner");
const par = document.getElementById("par");
function start(){
const target = 0;
animate(ele,{
speed:10,
attrs:{
left:"500px",
top:par.offsetHeight - ele.offsetHeight*0.5 - 2,
opacity:1,
},
done(){
animate(ele,
{
speed:20,
attrs:{
left:0,
top:ele.offsetHeight*0.5
}
}
);
}
});
}
...
是不是很简单呢?我们来看一下效果:
链式运动.gif解决回调函数嵌套
链式运动的一个缺陷在于:如果需要进行多次链式运动,那么会陷入层层的回调函数中,为了解决这个问题,我们引入 async/await 和 Promise。修改 animate 函数:
function animate(ele = null,config = {
speed:10,
attrs:{},
}){
// 清除定时器
clearInterval(ele.timer);
const attrs = config.attrs;
return new Promise((resolve)=>{
ele.timer = setInterval(()=>{
let flag = true;
for(const attr in attrs){
let speed = config.speed;
// 设置目标距离
let target = Number.parseInt(attrs[attr]);
// 获取当前的样式
let currentStyle = (attr === "opacity")?(Number.parseInt(Number.parseFloat(getCurrentStyle(ele,attr))*100)):Number.parseInt(getCurrentStyle(ele,attr));
// 如果改变的样式是 opacity,target乘以100
if(attr === "opacity"){
target = Number.parseInt(Number.parseFloat(attrs[attr])*100);
}
// 根据当前样式值和目标位置的差值判断速度方向
if(currentStyle - target > 0){
speed = -speed;
}
// 根据运动是否完成来设定 flag
if(currentStyle !== target){
flag = false;
}
// 运动到目标点后清除定时器
if(Math.abs(target - currentStyle) < Math.abs(speed)){
ele.style[attr] = (attr === "opacity")? target / 100 : target + "px";
}else{
// 根据当前样式动态改变物体的样式
ele.style[attr] = (attr === "opacity")?( currentStyle + speed)/100:(currentStyle + speed) + "px";
currentStyle += speed;
}
}
if(flag){
clearInterval(ele.timer);
resolve();
}
},30);
});
}
调用运动函数:
...
const ele = document.getElementById("inner");
const par = document.getElementById("par");
async function start(){
await animate(ele,{
speed:10,
attrs:{
left:"500px",
top:par.offsetHeight - ele.offsetHeight*0.5 - 2,
opacity:1,
}
});
await animate(ele,{
speed:10,
attrs:{
left:"0",
top:ele.offsetHeight*0.5,
opacity:0.3,
}
});
}
...
查看效果:
链式运动 Promise 版.gif我们去掉了 config 中的 done 方法,改为 Promise 实现,这样一来,就可以避免回调函数地狱了。
总结
本文我们对 animate 进行了以下改进:
- 支持多值运动
- 多值运动时停止定时器的问题
- 支持链式运动
- 将链式运动由回调形式改为 Promise 形式
至此,animate 函数基本上已经完善了,当然还有一些需要优化的地方:
- 我们的单位 "px" 是硬编码,为了适应移动端,可以将 "em" 等作为参数传入
- 在修改 style 时,还可以对 config 中的属性进行判断,以确认 style 有该属性
- 其他可能的优化
完。