追踪为什么一个React组件会重新渲染

416

在 React 中,有没有一种系统性的方法来调试导致组件重新渲染的问题?我使用了一个简单的 console.log() 来查看它被渲染的次数,但是我很难弄清楚是什么原因导致组件多次渲染,比如在我的情况下渲染了 4 次。是否存在一种工具,可以显示时间线和/或所有组件树的渲染顺序?


也许你可以使用 shouldComponentUpdate 来禁用自动组件更新,然后从那里开始跟踪。更多信息可以在这里找到:https://facebook.github.io/react/docs/optimizing-performance.html - Reza Sadr
3
@jpdelatorre的回答是正确的。一般来说,React的优势之一是您可以通过查看代码轻松地追踪数据流返回链。React DevTools扩展程序可以帮助实现这一点。此外,我在我的Redux插件目录中有一个有用的工具列表,用于可视化/跟踪React组件重新渲染,以及一些关于[React性能监控](htt - markerikson
6
请查看 https://github.com/welldone-software/why-did-you-render。 - tokland
我已经尝试过这种方法,它非常适用于定位由钩子触发的重新渲染 https://github.com/facebook/react/issues/16477#issuecomment-591546077 - Abdelsalam Shahlol
8个回答

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

如果您需要一个不依赖于外部库的简短代码片段,我发现这个很有用。

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

6
我认为没有必要使用 if 语句块。简短而简洁。 - Isaac
2
除此之外,如果您发现状态的某个部分正在更新,但不清楚在哪里或为什么进行更新,您可以用 setState(...args) { super.setState(...args) } 覆盖 setState 方法(在类组件中),然后在调试器中设置一个断点,这样您就可以追踪到设置状态的函数。 - redbmk
1
@DawsonB,你可能在那个组件中没有任何状态,所以this.state是未定义的。 - Jacob Rask
9
我定义了 useTraceUpdate 并在我的函数组件中使用它。但它没有检测到道具的变化。该组件仍然会渲染两次(我在其中放置了 console.log("call!"),并在浏览器的控制台中得到了两个输出)。我还能做些什么? - Marecky
1
这仅适用于由于更改props而引起的重新渲染,而不适用于从其他hooks(useState,useReducer,useContext,...)更改状态。 - Sigfried
显示剩余4条评论

285
您可以使用React Devtools性能分析工具检查组件重新渲染的原因,无需更改代码。请参见React团队的博客文章Introducing the React Profiler

首先,进入设置齿轮 > profiler,并选择“记录每个组件重新渲染的原因”

React Dev Tools > Settings

React Devtools profiler的屏幕截图


6
火狐浏览器链接:https://addons.mozilla.org/zh-CN/firefox/addon/react-devtools/ - Akaisteph7
3
由于明显的原因,这应该是被接受的答案,因为它可以让您轻松地进行性能分析并准确定位问题的根源,而无需在代码库中创建丑陋的脚本。 - Mel Macaluso
5
有一件事情促使我使用类似 https://dev59.com/CFgR5IYBdhLWcg3wr_AY#51082563 的脚本,那就是当组件重新渲染的原因为 "Props Changed" 时,React DevTools 不会显示“旧”和“新” props 之间的差异。许多情况下这并不重要,但当 props 是值时,了解引起重新渲染的原因会有所帮助。此外,它还非常适合查看 hooks 再次运行的原因。 - Jorge Barreto
7
由于父组件的渲染,此代码未能按预期显示渲染,并且几乎总是无法显示重新渲染的原因。 - Eliav Louski
4
我们如何确定是哪个钩子或者哪个上下文发生了变化呢?仅仅说“钩子变了”或者“上下文变了”并不是很有帮助。 - darKnight
显示剩余2条评论

91

以下是一些 React 组件会重新渲染的例子:

  • 父组件重新渲染
  • 在组件内调用 this.setState()。这将触发以下组件生命周期方法:shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate
  • 组件的 props 发生变化。这将触发 componentWillReceiveProps > shouldComponentUpdate > componentWillUpdate > render > componentDidUpdate(当 Redux store 中的应用程序发生适用的更改时,react-reduxconnect 方法会触发此操作)
  • 调用类似于 this.setStatethis.forceUpdate

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

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


10
注意: "无状态" 只是指不使用状态的任何组件,无论是用类语法还是函数语法定义的。此外,函数组件总是重新渲染。您需要使用 shouldComponentUpdate 或扩展 React.PureComponent,以强制仅在更改时重新渲染。 - markerikson
1
你说的无状态/函数组件总是重新渲染是正确的。我会更新我的答案。 - jpdelatorre
1
因此,即使您使用函数式创建组件的方式,例如 const MyComponent = (props) => <h1>Hello {props.name}</h1>;(这是一个无状态组件),它也会在父组件重新渲染时重新渲染。 - jpdelatorre
2
这确实是一个很好的答案,但它并没有回答真正的问题——如何跟踪触发重新渲染的原因。Jacob R的答案看起来很有希望能够解决实际问题。 - Sanuj
1
任何通过useContext钩子实现的上下文消费者中的更改也会导致重新渲染,而不是通过<SomeContext.Consumer>... - elMeroMero
显示剩余3条评论

12

@jpdelatorre的回答很好地强调了React组件可能重新渲染的一般原因。

我只是想深入探讨其中一个实例:当props改变时。解决导致React组件重新渲染的问题是一个常见的问题,在我的经验中,很多时候跟踪此问题涉及确定哪些props正在更改

React组件每当接收到新的props时都会重新渲染。 它们可以像这样接收新的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 个 prop 时,通过在 render 块的开始处加入 console.log(this.props) 来追踪哪些 prop 更改并不太困难。但是对于更复杂的组件和更多的 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)
}

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


3
现在它被称为UNSAFE_componentWillReceiveProps(nextProps),并且被弃用了。_"之前这个生命周期的名称是componentWillReceiveProps。那个名称会一直工作到17版本。"_ 来自 React 文档 - Emile Bergeron
1
你可以通过componentDidUpdate实现相同的功能,这可能更好,因为你只想找出是什么导致了组件的实际更新。 - see sharper

8

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

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(高阶组件),因为有时你已经在顶部解构了props,而Jacob的解决方案并不适合。

免责声明:本人与软件包所有者没有任何关联。仅仅是为了尝试在深层嵌套的对象中找到差异而进行无数次点击,真是痛苦不堪。


2
为了帮助其他人节省一些搜索时间:npm deep-diffdeep-diff源代码在github上的链接。(源代码链接是npm页面上的“repository”链接。) - ToolmakerSteve
1
在确定属性值是否更改时,最好使用引用比较。React在props中使用引用比较,因此如果您有两个不同的['a']实例,则deep_diff将报告它们未更改,但React会说它们已更改。但是,对于日志输出使用deep_diff没有问题。 - rpggio

7
感谢https://dev59.com/CFgR5IYBdhLWcg3wr_AY#51082563的答案,我为仅适用于功能组件(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://dev59.com/CFgR5IYBdhLWcg3wr_AY#51082563
 */
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编写的。


1
运行得很好。如果这能作为一个小的npm包发布就更好了。 - nachtigall
是的,也许有一天如果我找到时间的话!:D 可能会使用TSDX作为起点。 - Vadorequest

6

使用钩子和函数组件,不仅仅是属性改变会导致重新渲染。我开始使用的是一种相当手动的日志记录方法。它对我帮助很大。你可能也会发现它有用。

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

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

然后在每个“可疑”的调用(props,hooks)之后我写下了:
const example = useExampleHook();
checkDep(refs, 'example ', example);

3
上面的答案非常有帮助,如果有人正在寻找检测重新渲染原因的特定方法,那么我发现这个库redux-logger非常有用。
你可以添加这个库并启用状态之间的差异(文档中有说明),例如:
const logger = createLogger({
    diff: true,
});

将以下文本翻译成中文:

并在store中添加中间件。

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

然后您可以运行应用程序并检查控制台日志。无论何时有一个日志,它都会在显示状态(nextProps和this.props)之间的差异,并且您可以决定是否真的需要在那里进行渲染enter image description here

它将类似于上面的图像,以及diff键。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接