react-redux componentWillMount 递归调用导致内存溢出问题排查

在学习 redux react-redux 模拟实现过程中, 为了方便派发 dispatch 使react-redux 接受一个 mapDispatchToProps 函数, 内部将 dispatch 和 props 传入并接受调用的返回值, 由使用者自行定义触发的 dispatch 事件, 通过高阶组件的形式再通过 props 流向目标组件。

react-redux 实现如下:

import React from "react";
import PropTypes from "prop-types";

export const connect = (mapStateToProps, mapDispatchToProps) => WrappedComponent => {
  // mapStateToProps 保存在闭包环境中
  return class Connect extends React.Component {
    static contextTypes = {
      store: PropTypes.object
    };

    constructor(props) {
      super(props);
      this.state = {
        allProps: {}
      };
    }

    componentWillMount() {
      const { store } = this.context;
      this._updateProps();
      // state 已经更新
      store.subscribe(() => this._updateProps());
    }

    _updateProps() {
      const { store } = this.context;
      // 额外传入 props, 让获取数据更加灵活方便
      const stateProps = mapStateToProps?.(store.getState(), this.props) ?? {};
      const dispatchProps = mapDispatchToProps?.(store.dispatch, this.props) ?? {};
      this.setState({
        allProps: {
          ...stateProps,
          ...dispatchProps,
          ...this.props
        }
      })
    }

    render() {
      const { store } = this.context;
      const stateProps = mapStateToProps(store.getState());
      return <WrappedComponent {...this.state.allProps} />;
    }
  };
};

使用的组件 ThemeSwitch 组件代码如下:

import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "./util/react-redux";

class ThemeSwitch extends Component {
  static contextTypes = {
    themeColor: PropTypes.string,
    onSwitchColor: PropTypes.func
  };

  render() {
    const { themeColor, onSwitchColor } = this.props;
    return (
      <div>
        <button
          onClick={onSwitchColor('red')}
          style={{ color: themeColor }}
        >
          Red
        </button>
        <button
          onClick={onSwitchColor(this,'blue')}
          style={{ color: themeColor }}
        >
          Blue
        </button>
      </div>
    );
  }
}

const mapDispatchToProps = dispatch => {
  return {
    onSwitchColor(color) {
      dispatch({ type: "CHANGE_COLOR", themeColor: color });
    }
  };
};

const mapStateToProps = store => {
  return {
    themeColor: store.themeColor
  };
};

ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch);

export default ThemeSwitch;

运行后报错结果:

Maximum update depth exceeded. 
This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. 
React limits the number of nested updates to prevent infinite loops.

报错结果可以看出,堆栈溢出,因为重复调用某个生命周期钩子 componentWillUpdatecomponentDidUpdate 导致一直重新渲染。

首先排查了 react-redux 的代码,并且在别的组件都完全正常,所以问题原因就锁定在 ThemeSwitch 组件。
通过在 react-redux 文件的 _updatePropsdebugger 发现该方法会无限的调用,调用的原因无非第一次 componentWillMount 钩子中对其初始化一次,并且订阅了 store 中 state 的变化,那么之所以重读被调用,肯定是 store.subscribe 内一直发布事件,导致订阅函数一直被更新,所以一直调用 *_updateProps* 函数。

所以问题锁定在 state 一直被修改 state 数据定义在 redux 模拟实现中。redux 简单的实现如下:

function createStore(reducer) {
  let state = null;
  // 缓存旧的 state
  const listeners = [];
  const subscribe = listener => listeners.push(listener);
  const getState = () => state;
  const dispatch = action => {
    state = reducer(state, action);
    listeners.forEach(listener => listener());
  };
  dispatch({}); // 初始化 state
  return {
    getState,
    dispatch,
    subscribe
  };
}

发现 state 更新的唯一途径就是 dispatch 函数的触发,哪里无限调用 dispatch 函数了呢? 最后发现 ThemeSwitch 组件的给按钮绑定监听事件,修改主题色的函数是直接调用的 - -
就导致 dispatch 一直更新。无限循环导致内存泄露的流程:
onSwitchColor方法调用 -> 触发dispatch更新 -> _updateProps触发 -> setState更新状态 -> 更新目标组件Prop -> 目标组件重新 Render -> onSwitchColor方法调用 -> 触发dispatch更新

修改 ThemeSwitch 文件有问题的代码:

return (
      <div>
        <button
          onClick={onSwitchColor('red')}
          style={{ color: themeColor }}
        >
          Red
        </button>
        <button
          onClick={onSwitchColor('blue')}
          style={{ color: themeColor }}
        >
          Blue
        </button>
      </div>
    );

修改后:

return (
      <div>
        <button
          onClick={onSwitchColor.bind(this ,'red')}
          style={{ color: themeColor }}
        >
          Red
        </button>
        <button
          onClick={onSwitchColor.bind(this,'blue')}
          style={{ color: themeColor }}
        >
          Blue
        </button>
      </div>
    );

完美运行!

猜你喜欢

转载自www.cnblogs.com/qiqingfu/p/12431897.html
今日推荐