【问题标题】:Trace why a React component is re-rendering跟踪 React 组件重新渲染的原因
【发布时间】:2019-06-26 12:29:33
【问题描述】:

是否有系统的方法来调试导致组件在 React 中重新渲染的原因?我放了一个简单的 console.log() 来查看它渲染了多少次,但是在弄清楚是什么导致组件多次渲染时遇到了麻烦,在我的例子中是(4 次)。是否存在显示时间线和/或所有组件树渲染和顺序的工具?

【问题讨论】:

标签: reactjs redux


【解决方案1】:

如果你想要一个没有任何外部依赖的简短 sn-p,我觉得这很有用

componentDidUpdate(prevProps, prevState) {
  Object.entries(this.props).forEach(([key, val]) =>
    prevProps[key] !== val && console.log(`Prop '${key}' changed`)
  );
  if (this.state) {
    Object.entries(this.state).forEach(([key, val]) =>
      prevState[key] !== val && console.log(`State '${key}' changed`)
    );
  }
}

这是我用来跟踪功能组件更新的一个小钩子

function useTraceUpdate(props) {
  const prev = useRef(props);
  useEffect(() => {
    const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
      if (prev.current[k] !== v) {
        ps[k] = [prev.current[k], v];
      }
      return ps;
    }, {});
    if (Object.keys(changedProps).length > 0) {
      console.log('Changed props:', changedProps);
    }
    prev.current = props;
  });
}

// Usage
function MyComponent(props) {
  useTraceUpdate(props);
  return <div>{props.children}</div>;
}

【讨论】:

  • @yarden.refaeli 我认为没有理由设置 if 块。简明扼要。
  • 除此之外,如果您发现某个状态正在被更新,并且在哪里或为什么不明显,您可以使用setState(...args) { super.setState(...args) } 覆盖setState 方法(在类组件中)然后在你的调试器中设置一个断点,然后你就可以追溯到设置状态的函数。
  • 钩子函数到底如何使用?我在你写的时候定义了useTraceUpdate,到底应该在哪里打电话?
  • 在函数组件中,你可以像function MyComponent(props) { useTraceUpdate(props); }这样使用它,它会在props发生变化时记录
  • @DawsonB 您可能在该组件中没有任何状态,因此 this.state 未定义。
【解决方案2】:

以下是 React 组件将重新渲染的一些实例。

  • 父组件重新渲染
  • 在组件内调用this.setState()。这将触发以下组件生命周期方法shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate
  • 组件props 的更改。这将触发 componentWillReceiveProps > shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate(react-redux 的 connect 方法在 Redux 存储中有适用更改时触发此)
  • 调用类似于this.setState的this.forceUpdate

您可以通过在您的shouldComponentUpdate 中实施检查并在不需要时返回false 来最小化组件的重新渲染。

另一种方法是使用React.PureComponent 或无状态组件。纯组件和无状态组件仅在其 props 发生更改时才会重新渲染。

【讨论】:

  • Nitpick:“无状态”只是指任何不使用状态的组件,无论它是用类语法还是函数语法定义的。此外,功能组件总是重新渲染。您需要使用shouldComponentUpdate 或扩展React.PureComponent,以强制仅在更改时重新渲染。
  • 关于无状态/功能组件总是重新渲染是对的。将更新我的答案。
  • 因此,即使您使用创建组件的功能方式,例如const MyComponent = (props) =&gt; &lt;h1&gt;Hello {props.name}&lt;/h1&gt;;(这是一个无状态组件)。每当父组件重新渲染时,它都会重新渲染。
  • 这肯定是一个很好的答案,但它没有回答真正的问题, - 如何追踪触发重新渲染的原因。 Jacob R 的回答看起来很有希望为实际问题提供答案。
  • 当通过 useContext-hook 而不是 &lt;SomeContext.Consumer&gt;... 实现时,上下文消费者的任何更改也会导致重新渲染。
【解决方案3】:

您可以使用 React Devtools 分析器工具检查组件(重新)渲染的原因。无需更改代码。请参阅 React 团队的博文Introducing the React Profiler。

首先,转到设置 cog > profiler,然后选择“记录每个组件呈现的原因”

【解决方案4】:

@jpdelatorre 的回答非常适合突出 React 组件可能重新渲染的一般原因。

我只是想更深入地研究一个实例:当道具改变时。对导致 React 组件重新渲染的原因进行故障排除是一个常见问题,根据我的经验,很多时候跟踪此问题涉及确定哪些 props 正在更改。

React 组件在收到新道具时会重新渲染。他们可以收到新的道具,例如:

&lt;MyComponent prop1={currentPosition} prop2={myVariable} /&gt;

或者如果MyComponent 连接到redux store:

function mapStateToProps (state) {
  return {
    prop3: state.data.get('savedName'),
    prop4: state.data.get('userCount')
  }
}

任何时候prop1、prop2、prop3 或prop4 的值更改MyComponent 都会重新渲染。使用 4 个道具,通过在 render 块的开头放置一个 console.log(this.props) 来追踪哪些道具正在改变并不难。然而,随着组件越来越复杂,道具越来越多,这种方法是站不住脚的。

这是一个有用的方法(为了方便,使用lodash)来确定哪些道具更改导致组件重新渲染:

componentWillReceiveProps (nextProps) {
  const changedProps = _.reduce(this.props, function (result, value, key) {
    return _.isEqual(value, nextProps[key])
      ? result
      : result.concat(key)
  }, [])
  console.log('changedProps: ', changedProps)
}

将这个 sn-p 添加到您的组件可以帮助揭示导致可疑重新渲染的罪魁祸首,而且很多时候这有助于阐明不必要的数据被输送到组件中。

【讨论】:

  • 它现在被称为UNSAFE_componentWillReceiveProps(nextProps) 并且已被弃用。 “此生命周期以前被命名为 componentWillReceiveProps。该名称将在版本 17 之前继续有效。”来自 React documentation。
  • 您可以使用 componentDidUpdate 实现相同的效果,无论如何这可以说更好,因为您只想找出导致组件实际更新的原因。
【解决方案5】:

奇怪的是没有人给出这个答案,但我发现它非常有用,特别是因为道具更改几乎总是嵌套很深。

Hooks 粉丝:

import deep_diff from "deep-diff";
const withPropsChecker = WrappedComponent => {
  return props => {
    const prevProps = useRef(props);
    useEffect(() => {
      const diff = deep_diff.diff(prevProps.current, props);
      if (diff) {
        console.log(diff);
      }
      prevProps.current = props;
    });
    return <WrappedComponent {...props} />;
  };
};

“老派”粉丝:

import deep_diff from "deep-diff";
componentDidUpdate(prevProps, prevState) {
      const diff = deep_diff.diff(prevProps, this.props);
      if (diff) {
        console.log(diff);
      }
}

附:我还是更喜欢使用 HOC(高阶组件),因为有时你在顶部解构了你的道具,而 Jacob 的解决方案并不适合

免责声明:与包所有者没有任何关系。只需单击数十次以尝试找出嵌套较深的对象的差异是一件很痛苦的事情。

【讨论】:

【解决方案6】:

使用钩子和函数式组件,不只是 prop 更改会导致重新渲染。我开始使用的是一个相当手动的日志。这对我帮助很大。您可能会发现它也很有用。

我将这部分复制到组件的文件中:

const keys = {};
const checkDep = (map, key, ref, extra) => {
  if (keys[key] === undefined) {
    keys[key] = {key: key};
    return;
  }
  const stored = map.current.get(keys[key]);

  if (stored === undefined) {
    map.current.set(keys[key], ref);
  } else if (ref !== stored) {
    console.log(
      'Ref ' + keys[key].key + ' changed',
      extra ?? '',
      JSON.stringify({stored}).substring(0, 45),
      JSON.stringify({now: ref}).substring(0, 45),
    );
    map.current.set(keys[key], ref);
  }
};

在方法的开头我保留了一个 WeakMap 引用:

const refs = useRef(new WeakMap());

然后在每次“可疑”调用(道具、钩子)之后我写:

const example = useExampleHook();
checkDep(refs, 'example ', example);

【讨论】:

    【解决方案7】:

    感谢https://stackoverflow.com/a/51082563/2391795 的回答,我为功能组件(TypeScript)提出了这个略有不同的解决方案,它还处理状态而不仅仅是道具。

    import {
      useEffect,
      useRef,
    } from 'react';
    
    /**
     * Helps tracking the props changes made in a react functional component.
     *
     * Prints the name of the properties/states variables causing a render (or re-render).
     * For debugging purposes only.
     *
     * @usage You can simply track the props of the components like this:
     *  useRenderingTrace('MyComponent', props);
     *
     * @usage You can also track additional state like this:
     *  const [someState] = useState(null);
     *  useRenderingTrace('MyComponent', { ...props, someState });
     *
     * @param componentName Name of the component to display
     * @param propsAndStates
     * @param level
     *
     * @see https://stackoverflow.com/a/51082563/2391795
     */
    const useRenderingTrace = (componentName: string, propsAndStates: any, level: 'debug' | 'info' | 'log' = 'debug') => {
      const prev = useRef(propsAndStates);
    
      useEffect(() => {
        const changedProps: { [key: string]: { old: any, new: any } } = Object.entries(propsAndStates).reduce((property: any, [key, value]: [string, any]) => {
          if (prev.current[key] !== value) {
            property[key] = {
              old: prev.current[key],
              new: value,
            };
          }
          return property;
        }, {});
    
        if (Object.keys(changedProps).length > 0) {
          console[level](`[${componentName}] Changed props:`, changedProps);
        }
    
        prev.current = propsAndStates;
      });
    };
    
    export default useRenderingTrace;
    

    请注意,实现本身并没有太大变化。文档展示了如何将它用于 props/states,并且该组件现在是用 TypeScript 编写的。

    【讨论】:

    • 效果很好。要是能以 npm 包的形式发布就好了。
    • 是的,如果我有时间的话,也许有一天! :D 可能会使用 TSDX 作为启动器。
    【解决方案8】:

    以上答案非常有帮助,以防万一有人正在寻找特定方法来检测重新渲染的原因,那么我发现this library redux-logger 非常有帮助。

    您可以做的是添加库并启用状态之间的差异(它在文档中),例如:

    const logger = createLogger({
        diff: true,
    });
    

    并在 store 中添加中间件。

    然后在你要测试的组件的渲染函数中放一个console.log()。

    然后你可以运行你的应用程序并检查控制台日志。只要有日志,它就会显示状态(nextProps and this.props)之间的差异,你可以决定那里是否真的需要渲染

    它与上图类似,还有 diff 键。

    【讨论】:

      猜你喜欢
      • 2021-06-19
      • 2018-04-22
      • 1970-01-01
      • 1970-01-01
      • 2016-01-11
      • 2020-05-01
      • 2018-03-24
      相关资源
      最近更新 更多