react项目中利用多个setTimeout实现动画效果的隐藏bug

最近遇到一个看似比较轻松,但实际上实现起来有点扎手的功能,这是我自己的感受啊~不代表任何人。

实现的功能就是登录页上有一组动画,大概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 })
  }

这样处理了之后,才是真的解决了这个问题。

另外,如果有优化代码的更好方法,希望小伙伴留言噢~

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值