ChatGPT解决这个技术问题 Extra ChatGPT

跟踪 React 组件重新渲染的原因

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

也许您可以使用 shouldComponentUpdate 禁用自动组件更新,然后从那里开始您的跟踪。可在此处找到更多信息:facebook.github.io/react/docs/optimizing-performance.html
@jpdelatorre 的答案是正确的。一般来说,React 的优势之一是您可以通过查看代码轻松地追溯数据流回链。 React DevTools extension 可以提供帮助。另外,我有一个 useful tools for visualizing/tracking React component re-rendering 列表作为我的 Redux addons catalog 的一部分,以及一些关于 [React 性能监控](htt
我试过这个方法,找到钩子触发的重新渲染非常好github.com/facebook/react/issues/16477#issuecomment-591546077

J
Jacob Rask

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

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 之后,我究竟应该在哪里调用它?
@DawsonB 您可能在该组件中没有任何状态,因此 this.state 未定义。
我定义了 useTraceUpdate 并在我的函数组件中使用了它。但它没有检测到道具发生了变化。并且组件仍然呈现两次(我将 console.log("call!") 放入其中,并在浏览器的控制台中得到两个打印输出。我还能做什么?
K
KyleMit

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

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

https://i.stack.imgur.com/HCpFy.png

https://i.stack.imgur.com/pQIKi.gif


出于显而易见的原因,这应该是公认的答案,因为它可以让您轻松分析并查明原因,而无需在代码库中创建丑陋的脚本。
导致我使用类似于 stackoverflow.com/a/51082563 的脚本的一件事是,当渲染的原因是“道具已更改”时,React DevTools 不会显示“旧”和“新”道具之间的差异。很多时候这并不重要,但是当道具是值时,它可以帮助查看导致重新渲染的原因。它也很适合了解为什么钩子会再次运行。
j
jpdelatorre

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

父组件重新渲染

在组件内调用 this.setState()。这将触发以下组件生命周期方法 shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate

组件道具的变化。这将触发 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) => <h1>Hello {props.name}</h1>;(这是一个无状态组件)。每当父组件重新渲染时,它都会重新渲染。
这肯定是一个很好的答案,但它没有回答真正的问题, - 如何追踪触发重新渲染的原因。 Jacob R 的答案看起来很有希望为实际问题提供答案。
当通过 useContext-hook 而不是 <SomeContext.Consumer>... 实现时,上下文使用者的任何更改也会导致重新呈现。
C
Cumulo Nimbus

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

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

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

<MyComponent prop1={currentPosition} prop2={myVariable} />

或者如果 MyComponent 连接到 redux 存储:

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

任何时候 prop1prop2prop3prop4 的值更改 MyComponent 都会重新呈现。对于 4 个 props,通过在 render 块的开头放置一个 console.log(this.props) 来跟踪哪些 props 正在更改并不难。然而,随着组件越来越复杂,道具越来越多,这种方法是站不住脚的。

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

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

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


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

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

钩子迷们:

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);
      }
}

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

免责声明:与包所有者没有任何关系。只需单击数十次以尝试发现深层嵌套对象的差异是一种痛苦。


为其他人节省一些谷歌搜索:npm deep-diffdeep-diff source at github。 (源链接是 npm 页面上的“存储库”链接。)
判断 prop 值是否发生变化时最好使用 ref 比较。 React 对 props 使用引用比较,因此如果您有两个不同的 ['a'] 实例,deep_diff 会报告它们未更改,但 React 会说它们已更改。不过,使用 deep_diff 作为日志输出没有问题。
M
Miklos Jakab

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

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

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);

V
Vadorequest

感谢 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;

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


效果很好。如果这是作为小 npm 包发布的,那就太好了。
是的,如果我有时间的话,也许有一天! :D 可能会使用 TSDX 作为启动器。
p
pritesh

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

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

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

并在 store 中添加中间件。

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

https://i.stack.imgur.com/QwR2o.png

它将类似于上图以及 diff 键。