最近遇到一个看似比较轻松,但实际上实现起来有点扎手的功能,这是我自己的感受啊~不代表任何人。
实现的功能就是登录页上有一组动画,大概12个,前10个动画之间间隔0.5s左右,第10个与第11个动画之间间隔2s,后面依旧是间隔0.5s。(可查看下面的动态图)
首先所有的动画均不可见,然后第一个动画出现后不消失,接着0.5s后再出现第二个动画,也不消失,直到12个动画展示完毕之后,大概间隔5s左右,所有可见的动画全部为不可见,然后再次从第一个动画开始继续展示,依次展示和循环下去。
效果如下:
首先看是怎么实现的,我实现的方式very very very low!!!,就是算准动画出现的时间后,用setTimeout实现的,本来想的是用Promise链式调用实现,可是后来仔细想了想,Promise的链式调用本质上与setTimeout没多大区别,只是加了三个状态而已。
而且目前的我对Promise的具体实现说实话还差点火候,所以选择单纯的setTimout回调(俗称回调地狱),我的目的是先把功能实现,然后再优化性能。
实现如下:
CSS:(一个动画为例)
// gif动图中,电脑屏幕内左大图
.img-contents {
position: absolute;
top: 80px;
left: 350px;
z-index: 8;
opacity: 0;
&.isActive {
opacity: 1;
animation: contents 0.5s;
-webkit-animation: contents 0.5s;
}
}
@keyframes contents {
0% {
transform: scale(0, 0);
}
10% {
transform: scale(0.1, 0.1);
}
20% {
transform: scale(0.2, 0.2);
}
30% {
transform: scale(0.4, 0.4);
}
40% {
transform: scale(0.8, 0.8);
}
50% {
transform: scale(1.1, 1.1);
}
100% {
transform: scale(1, 1);
}
}
这里没有用到 CSS3 animation 属性的 delay 功能,是因为动画初始是没有的,最终形态确是存在于屏幕上的,而CSS动画效果显示完毕之后,它会回到最初的状态。
HTML:
import contents from '../../statics/login_bg_contents.png';
<img className={this.state.contentsAnimation ? 'img-contents isActive' : 'img-contents'} src={contents}></img>
12个 CSS 动画写完之后,便开始逻辑显示了:
JS:(这个代码巨丑!!!无法直视!!!)
setTimeout(function(){
this.setState({contentsAnimation:true})
setTimeout(function(){
this.setState({chartAnimation:true})
setTimeout(function(){
this.setState({checkAnimation:true})
setTimeout(function(){
this.setState({aniDialog1:true})
setTimeout(function(){
this.setState({aniDialog2:true})
setTimeout(function(){
this.setState({aniDialog3:true})
...
...
...
...
...
...
},500)
},500)
},500)
},500)
},500)
},500)
怎么样?意不意外?惊不惊喜?我老大看到了之后瞪大了眼睛!他简直不敢相信自己的眼睛!我自己特么都不信!哈哈哈哈嗝~
我知道这完全不可取,所以我赶紧跟他解释:先实现功能,再优化性能~~~嘤嘤嘤~~~
初始结果:完美实现!但是可怕地方在于,产品小姐姐说,这组动画是
无限次循环的......
限次循环的......
次循环的......
循环的......
环的......
的......
.......
我特么的心态爆炸了都 !!!可是能怎么办呢?毕竟是产品需求,其他产品组都是这么做的(别人家的动画我看了,根本没我这个复杂,偏简单些),那行趴,继续做呗...
每组动画间大概间隔4-5s,那么比较好做的就是把,上面的代码(想到此我就想笑,噗~)进行封装,然后再利用一个setTimeout将其调用即可,嗯哼,想法很好,实现的也基本OK了,现在就要进行性能优化了。
之前有想到过利用Promise,Promise可较好的解决回调地狱的问题,解决方式主要是注入了三个状态,但如果不想通过回调的方式实现呢?而是通过同步的方式实现,可不可以呢?
答案是可以的,只不过定时器的个数是不会少的,具体实现如下:
componentDidMount() {
this.loopAnimation();
}
loopAnimation(){
this.isActiveAnimation({contentsAnimation: true}, 500);
this.isActiveAnimation({chartAnimation: true}, 1000);
this.isActiveAnimation({checkAnimation: true}, 1500);
this.isActiveAnimation({ aniDialog1: true, aniDialogEtc: true }, 2000);
this.isActiveAnimation({ aniDialog2: true, aniDialogData: true }, 2500);
this.isActiveAnimation({ aniDialog3: true, aniDialogContents: true }, 3000);
this.isActiveAnimation({ aniDialogData: false, aniDialogEtc2: true }, 4000);
this.isActiveAnimation({ aniDialogTag: true, aniDialogContents: false, aniDialogEtc: false }, 4500);
this.isActiveAnimation({ aniDialogEtc3: true }, 5000);
this.isActiveAnimation({ aniDialogEtc2: false, aniDialogData: true }, 7000);
this.isActiveAnimation({ aniDialogEtc3: false, aniDialogContents: true }, 7500);
// 因为动画一直循环,每到下一次循环前初始化
this.isActiveAnimation({
contentsAnimation: false,
chartAnimation: false,
checkAnimation: false,
aniDialog1: false,
aniDialog2: false,
aniDialog3: false,
aniDialogTag: false,
aniDialogData: false,
aniDialogEtc: false,
aniDialogEtc2: false,
aniDialogEtc3: false,
aniDialogContents: false
},12000)
const _this = this;
setTimeout(function(){
_this.loopAnimation();
},12000)
}
isActiveAnimation(params, delay){
const _this = this;
setTimeout(function(){
_this.setState(params);
}, delay)
}
等到去登录页观看效果时,会欣慰一笑,效果实现了!看到这里,如果有小伙伴有更好的优化方法,欢迎评论交流哦~
--------------------------------------------------------阿西吧分割线----------------------------------------------------------------
但是一旦点击“登录”按钮后,进入到首页时,会报如下的错误:
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
意思也很明显,就是在页面卸载之后,不能再进行异步操作
问题的根源所在在哪呢?我们都知道是页面卸载之后,设置的定时器并没有清除!所以,这个bug的解决方法就是怎么清除定时器。那么该怎么具体解决这个bug呢?也就是说怎么在页面卸载的时候停止异步操作,即componentWillUnmount(){}中不进行异步操作。
每设置一个setTimeout,都会产生一个timerId,这个timerId会对应指定的定时器,只要进行clearTimeout(timerId)即可清除对应的定时器,那么该怎么不会遗漏的清除所有的定时器呢?
方法是:每设置一个定时器时,都将其push进一个数组中,该数组用于存放定时器,然后在页面卸载的时候,将其进行遍历,一个一个的清除即可,具体JS实现:
constructor(props) {
super(props);
this.state = {
contentsAnimation: false,
chartAnimation: false,
checkAnimation: false,
aniDialog1: false,
aniDialog2: false,
aniDialog3: false,
aniDialogTag: false,
aniDialogData: false,
aniDialogEtc: false,
aniDialogEtc2: false,
aniDialogEtc3: false,
aniDialogContents: false,
timerArray: []
}
}
componentWillUnmount(){
const { timerArray } = this.state;
this.clearTimeouts(timerArray);
}
clearTimeouts(arr){
for (let i = arr.length - 1; i >= 0; i--) {
if (typeof arr[i] !== 'undefined') {
clearTimeout(arr[i]);
}
}
}
isActiveAnimation(params, delay){
const _this = this;
let { timerArray } = _this.state;
timerArray.push(
setTimeout(function(){
_this.setState(params);
}, delay)
)
_this.setState({ timerArray })
}
会发现,完美解决问题~嘤嘤嘤~~嗨森~
--------------------------------------------------------------谢特分割线----------------------------------------------------------------
只要你足够细心,你会发现,如果用户在第二组动画未开始之前就点击了“登录”按钮,则会没有任何问题,但是一旦第二组动画开始,然后用户再点击“登录”按钮,还是会报同样的错,这是为什么?
仔细检查会发现,启动第二组动画的是谁?是loopAnimation()函数,与启动单独动画函数isActiveAnimation()无关,因此我们还需要将启动一组动画的setTimeout给清除:
constructor(props){
super(props)
this.state = {
timerLoop: []
}
}
componentWillUnmount(){
this.clearTimeouts(timerLoop);
}
loopAnimation(){
const _this = this;
let { timerLoop } = _this.state;
timerLoop.push(
setTimeout(function(){
_this.loopAnimation();
},12000)
)
_this.setState({ timerLoop })
}
这样处理了之后,才是真的解决了这个问题。
另外,如果有优化代码的更好方法,希望小伙伴留言噢~