美文网首页程序员
React之setState调用unmount组件报警告

React之setState调用unmount组件报警告

作者: 哇塞田 | 来源:发表于2017-08-13 14:06 被阅读4083次

    最近公司比较忙,所以更新进度比较慢,一是因为要梳理以前的代码,二是因为赶进度!!!
    这个问题也是机缘巧合碰到了,正赶上这周公司网络无缘无故抽风(很慢),然后那天在调试代码的时候忽然发现浏览器报了一个这样的Warning:

    warn.png

    其实从字面上也大概可以了解个大概,是由于setState只能更新挂载完成的组件或者正在挂载的组件,那么定位这个BUG的第一步当然是先了解组件的生命周期

    生命周期

    之前我的一片文章其实已经浅略的介绍了下生命周期,这里在补充下,facebook官方把生命周期分为三类,每类对应的函数分别如下:

    Mounting:
    constructor()
    componentWillMount()
    render()
    componentDidMount()
    
    Updating:
    componentWillReceiveProps()
    shouldComponentUpdate()
    componentWillUpdate()
    render()
    componentDidUpdate()
    
    Unmounting:
    componentWillUnmount()
    

    其中这三大类对应的函数中带有Will的会在render之前调用,带有Did的会在render之后调用,这里着重强调一下shouldComponentUpdate()这个生命周期,因为这个生命周期的返回值将会影响其他生命周期是否执行,其中最值得关注的就是当返回flase的时候不会触发render(另外还有componentWillUpdate() componentDidUpdate()),所以这也就给了我们优化项目的空间.由于题目的报错是指我在Unmounting的时候调用了setState,所以我去排查项目,以为自己手滑写错了......但是结果我排查的时候发现我代码中并没有在componentWillUnmount()这个生命周期钩子里面做setState的逻辑,于是好奇心之下我就在每个生命周期钩子中都用了下setState,于是发现了一些有意思的事

    有意思的事

    code.png console.png
    这是在componetWillUpdate钩子中写了setState之后的效果,当receive的时候就会无限触发componetWillUpdate,这是为什么呢? 经过程序媛的帮助 @眼角浅蓝(链接在后面)http://www.jianshu.com/u/8385c9b70d89,加上自己翻看源码,大概理解如下:

    React源码中有这样一个函数:

    performUpdateIfNecessary.png

    这个函数负责update组件,我们可以看到当this._pendingStateQueue !=null 时会触发this.updateComponent, this.updateComponent函数的代码如下:

    updateComponent: function(
        transaction,
        prevParentElement,
        nextParentElement,
        prevUnmaskedContext,
        nextUnmaskedContext,
      ) {
        var inst = this._instance;
        invariant(
          inst != null,
          'Attempted to update component `%s` that has already been unmounted ' +
            '(or failed to mount).',
          this.getName() || 'ReactCompositeComponent',
        );
    
        var willReceive = false;
        var nextContext;
    
        // Determine if the context has changed or not
        if (this._context === nextUnmaskedContext) {
          nextContext = inst.context;
        } else {
          nextContext = this._processContext(nextUnmaskedContext);
          willReceive = true;
        }
    
        var prevProps = prevParentElement.props;
        var nextProps = nextParentElement.props;
    
        // Not a simple state update but a props update
        if (prevParentElement !== nextParentElement) {
          willReceive = true;
        }
    
        // An update here will schedule an update but immediately set
        // _pendingStateQueue which will ensure that any state updates gets
        // immediately reconciled instead of waiting for the next batch.
        if (willReceive && inst.componentWillReceiveProps) {
          if (__DEV__) {
            measureLifeCyclePerf(
              () => inst.componentWillReceiveProps(nextProps, nextContext),
              this._debugID,
              'componentWillReceiveProps',
            );
          } else {
            inst.componentWillReceiveProps(nextProps, nextContext);
          }
        }
    
        var nextState = this._processPendingState(nextProps, nextContext);
        var shouldUpdate = true;
    
        if (!this._pendingForceUpdate) {
          if (inst.shouldComponentUpdate) {
            if (__DEV__) {
              shouldUpdate = measureLifeCyclePerf(
                () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
                this._debugID,
                'shouldComponentUpdate',
              );
            } else {
              shouldUpdate = inst.shouldComponentUpdate(
                nextProps,
                nextState,
                nextContext,
              );
            }
          } else {
            if (this._compositeType === CompositeTypes.PureClass) {
              shouldUpdate =
                !shallowEqual(prevProps, nextProps) ||
                !shallowEqual(inst.state, nextState);
            }
          }
        }
    
        if (__DEV__) {
          warning(
            shouldUpdate !== undefined,
            '%s.shouldComponentUpdate(): Returned undefined instead of a ' +
              'boolean value. Make sure to return true or false.',
            this.getName() || 'ReactCompositeComponent',
          );
        }
    
        this._updateBatchNumber = null;
        if (shouldUpdate) {
          this._pendingForceUpdate = false;
          // Will set `this.props`, `this.state` and `this.context`.
          this._performComponentUpdate(
            nextParentElement,
            nextProps,
            nextState,
            nextContext,
            transaction,
            nextUnmaskedContext,
          );
        } else {
          // If it's determined that a component should not update, we still want
          // to set props and state but we shortcut the rest of the update.
          this._currentElement = nextParentElement;
          this._context = nextUnmaskedContext;
          inst.props = nextProps;
          inst.state = nextState;
          inst.context = nextContext;
        }
      },
    
    

    程序的最后几行我们可以知道当需要shouldUpdate的时候会执行this._performComponentUpdate这个函数,this._performComponentUpdate函数的源码如下:


    _performComponentUpdate.png

    我们可以发现这个方法在次调用了componentWillUpdate方法,但是当在componentWillUpdate周期中调用setState时就会触发最上边的performUpdateIfNecessary函数,所以一直循环下去...
    附一个生命周期执行顺序的图:

    lifeCycle.png

    这里通过源码在附一张生命周期是否可以写setState函数的图(这里的可以不可以不是指报错,而是指是否有必要或者是避免出现无限循环):

    image.png

    es6语法中getDefaultProps和getInitialState合并到构造器constrator中.

    回到主题

    感觉扯着扯着有点远了,那么这个bug怎么解决呢(我们暂且成为bug,其实不会影响程序运行),到底是怎么产生的呢,原来是因为我有一部分setState写在了fetch的回调函数里,但fetch还没结束时我已经卸载了这个组件,当请求结束后setState执行的时候会发现这个组件已经卸载了,所以才会报了这个warning...

    解决办法

    1.把所有类似这种的请求放在项目中那个永远不会卸载的组件里,比如最顶层组件,然后数据通过props分发传递,这也就是同样的逻辑为什么通过redux的dispatch不会报warning,因为redux其实就一个最顶层的state,然后组件通过connect把值与props相关联起来
    2.第一种方法很麻烦,我现在采用的也是第二种,这里要提一下,其实react早就知道这个问题了,所以在最开始版本有这样一个参数叫isMounted,大家可以打印一下组件里的this,就应该能看的到,与这个一起的还有一个参数叫做replaceState,isMounted这个函数返回true和false,代表组件是否已经卸载,有了这个参数之后我们就可以在fecth的回调函数里在添加一个判断,只有组件未卸载才触发 setState,但是很不巧,这两个api都被废弃了,所以现在我们就只能自己模拟一个这样的函数,自定义个属性,默认为false,然后在componentWillUnmount生命周期中把这个属性制为true,用法和isMounted一样...

    题外话

    react除了废弃这两个api之外,还有一个很有意思的接口,叫batchedUpdates,这个api主要是控制setState机制的(是否立即生效),有兴趣的同学可以查看下资料.

    相关文章

      网友评论

        本文标题:React之setState调用unmount组件报警告

        本文链接:https://www.haomeiwen.com/subject/bnpulxtx.html