跟踪React组件重新呈现的原因

jas*_*san 106 reactjs redux

是否有一种系统的方法来调试导致组件在React中重新渲染的内容?我放了一个简单的console.log()来查看它渲染了多少时间,但是我很难搞清楚导致组件多次渲染的原因,即(4次)在我的情况下.是否存在显示时间轴和/或所有组件树呈现和排序的工具?

Jac*_*ask 148

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

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`)
    );
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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>;
}
Run Code Online (Sandbox Code Playgroud)

  • 我定义了 `useTraceUpdate` 并在我的函数组件中使用了它。但它没有检测到 props 发生了变化。并且组件仍然渲染两次(我将 `console.log("call!")` 放入其中,并在浏览器的控制台中得到两个打印输出。我还能做什么? (9认同)
  • @ yarden.refaeli我认为没有理由拥有if块.简洁明了. (5认同)
  • 非常有帮助,谢谢你保持简单! (4认同)
  • 除此之外,如果您发现某个状态正在更新,并且不清楚在哪里或为什么更新,您可以使用 `setState(...args) { super.setState() 覆盖 `setState` 方法(在类组件中)。 setState(...args) }`,然后在调试器中设置一个断点,然后您就可以追溯到设置状态的函数。 (2认同)

jpd*_*rre 59

以下是React组件将重新呈现的一些实例.

  • 父组件重新渲染
  • this.setState()在组件内调用.这将触发以下组件的生命周期方法shouldComponentUpdate> componentWillUpdate> render>componentDidUpdate
  • 组件的变化props.这将触发componentWillReceiveProps> shouldComponentUpdate> componentWillUpdate> render> componentDidUpdate(connect的方法react-redux触发该当存在在终极版存储适用的变化)
  • 调用this.forceUpdate哪个类似于this.setState

您可以通过在内部执行检查shouldComponentUpdatefalse在不需要的情况下返回来最小化组件的重新呈现.

另一种方法是使用React.PureComponent 或无状态组件.纯粹和无状态组件只有在它的道具发生变化时才会重新渲染.

  • Nitpick:"无状态"只是指任何不使用状态的组件,无论是使用类语法还是函数语法定义的.此外,功能组件_always_重新渲染.您需要使用`shouldComponentUpdate`或扩展`React.PureComponent`,以强制仅对更改进行重新呈现. (6认同)
  • 这肯定是一个很好的答案,但它没有回答真正的问题, - 如何追踪触发重新渲染的原因。雅各布 R 的答案在给出实际问题的答案方面看起来很有希望。 (2认同)

luk*_*svo 50

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

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

React 开发工具 > 设置

React Devtools 分析器的屏幕截图

  • 我们如何确定哪个钩子或哪个上下文发生了变化?仅仅说“挂钩已更改”或“上下文已更改”并没有多大帮助。 (9认同)
  • 这不能按预期工作,因为由于父组件渲染,它不显示渲染,而且它几乎总是不显示重新渲染的原因 (8认同)
  • 火狐链接:https://addons.mozilla.org/en-US/firefox/addon/react-devtools/ (6认同)
  • 导致我使用类似于 /sf/answers/3575779441/ 的脚本的一件事是,React DevTools 不会显示“旧”和“新”道具之间的差异,因为渲染是“道具已更改”。很多时候这并不重要,但是当 props 是值时,它可以帮助查看导致重新渲染的原因。这对于了解 hooks 再次运行的原因也非常有用。 (5认同)
  • 出于显而易见的原因,这应该是公认的答案,因为它可以让您轻松分析并查明原因,而无需在代码库中创建丑陋的脚本。 (3认同)

Cum*_*bus 10

@ jpdelatorre的答案很好地突出了React组件可能重新渲染的一般原因.

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

React组件在收到新道具时会重新呈现.他们可以收到新的道具,如:

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

或者如果MyComponent连接到redux商店:

function mapStateToProps (state) {
  return {
    prop3: state.data.get('savedName'),
    prop4: state.data.get('userCount')
  }
}
Run Code Online (Sandbox Code Playgroud)

任何时候的价值prop1,prop2,prop3,或prop4改变MyComponent将重新呈现.使用4个道具,通过console.log(this.props)render块的那个开始处追踪哪些道具正在改变并不是很困难.然而,随着更复杂的组件和越来越多的道具,这种方法是站不住脚的.

这是一个有用的方法(为方便起见使用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)
}
Run Code Online (Sandbox Code Playgroud)

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

  • 现在称为“ UNSAFE_componentWillReceiveProps(nextProps)”,并且已弃用。_“此生命周期以前称为`componentWillReceiveProps`。该名称将一直使用到版本17。” _来自[React文档](https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops)。 (3认同)

Zen*_*tzi 8

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

勾搭粉丝:

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} />;
  };
};
Run Code Online (Sandbox Code Playgroud)

“老”派粉丝:

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

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

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

  • 为了节省其他人的一些谷歌搜索:[npm deep-diff](https://github.com/flitbit/diff),[github上的deep-diff源代码](https://github.com/flitbit/diff)。(源链接是 npm 页面上的“存储库”链接。) (2认同)

Vad*_*est 7

感谢/sf/answers/3575779441/的回答,我为功能组件(TypeScript)提出了这个略有不同的解决方案,它还处理状态而不仅仅是 props。

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 /sf/answers/3575779441/
 */
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;
Run Code Online (Sandbox Code Playgroud)

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


Dam*_*een 5

现在在npm上可以使用此功能:

https://www.npmjs.com/package/use-trace-update

(公开,我发表了)

  • 这实际上与Jacob发布的代码相同。本来可以相信他的。 (4认同)

Mik*_*kab 5

使用钩子和功能组件,不仅仅是 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);
  }
};
Run Code Online (Sandbox Code Playgroud)

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

const refs = useRef(new WeakMap());
Run Code Online (Sandbox Code Playgroud)

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

const example = useExampleHook();
checkDep(refs, 'example ', example);
Run Code Online (Sandbox Code Playgroud)