2

React何时才会进行组件重渲染

 2 years ago
source link: https://www.ttalk.im/2021/12/when-does-react-re-render-components.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

文章介绍了React会在什么条件或何时进行组件重新渲染,同时给予一些优化代码的建议,这会使React应用更加的流畅

原文地址 When does React re-render components?

React因仅更新已更改的UI部分来提供快速的用户体验而闻名。 在研究React的渲染性能时,有一些术语和概念可能难以理解。很长一段时间,我并不是100%清楚VDOM是什么,或者React如何决定重新渲染组件。

在本文的第一部分, 我将解释在React渲染中的最重要的概念,以及React如何决定重新渲染给定的组件。 在本文的最后一部分, 我将向你展示如何优化React应用程序的渲染性能。

React中的渲染

什么是渲染

如果我们想了解React渲染和重新渲染是如何工作的,最好了解一下库背后的逻辑是什么。

渲染 是一个可以在不同抽象层次上理解的术语。 根据上下文,它的含义略有不同。 无论如何,它最终描述了生成图像的过程。

为了更好的开始,我们需要先理解什么是DOM(文档对象模型):

W3C的文档对象模型(DOM)是一个平台和语言无关的界面,它允许程序和脚本动态访问和更新文档的内容、结构和样式。”

简而言之,这意味着标记语言HTML所编写出来的页面是通过DOM展现成用户在屏幕上观看的内容。

浏览器准许JavaScript通过API来修改DOM:全局变量document代表着HTML DOM的状态,并且它为我们提供了一系列函数来进行修改。我们可以通过包含document.write、Node.appendChild或Element.setAttribute等函数的DOM编程接口使用JavaScript来修改DOM。

什么是VDOM

然后我们在渲染之上添加了另外一层抽象,就是React的虚拟DOM(或者我们常说的VDOM)。它包含了我们React应用的元素。我们应用中的变化都会先应用到VDOM上。如果因此所产生的VDOM新状态需进行UI变更, ReactDOM 库将通过尝试仅更新需要更新的内容来高效率的完成这件事情。

举个例子,如果只有一个元素的属性发生变化,React只会通过调用document.setAttribute(或类似的东西)来更新HTML元素的属性。

其中红色点代表更新的DOM树。更新VDOM并不一定会触发真正DOM的更新

当VDOM更新了,React将其与之前的VDOM快照进行比较,然后仅更新真实DOM中发生更改的内容。如果没有任何改变,真正的DOM根本不会更新。 这种将旧VDOM与新VDOM进行比较的过程称为差异。

真正的DOM更新很慢,因为它们会导致浏览器重新绘制UI。 React通过尽可能减少真正DOM更新数量来提高更新效率。 因此我们必须意识到原生和虚拟DOM更新之间的区别。 (因为我们增加了一层,另外是原因是即便是VDOM,更新的代价也是很大的)。

想深入了解背后的工作机制,可以阅读React的文档协调(Reconciliation)

这对性能有什么影响?

当我们讨论React的渲染时,我们真正在讨论的是 render函数的执行,这个函数从来都不是简单的执行一次UI更新

让我们看一个例子:

const App = () => {
  const [message, setMessage] = React.useState('');
  return (
    <>
      <Tile message={message} />
      <Tile />
    </>
  );
};

在函数组件中,整个函数的执行相当于类组件中的渲染函数。

当父组件(在本例中为App)中的状态发生变化时,两个Tile组件将重新渲染,即使第二个组件甚至没使用Props。 这意味着渲染函数被调用了3次,但真实DOM修改只在显示消息的Tile组件中发生1次:

其中红色点代表渲染节点。在React中,这代表调用渲染函数。在真实DOM中,这代表重新绘制UI。

好消息是你不必太担心UI重绘的性能瓶颈。 因为React已经为你进行了优化。

但是坏消息是: 所有左侧的红色点代表这些组件的渲染函数都被执行了。

执行这些渲染函数有两个缺点:

  1. React必须在每个组件上运行其差异算法,以检查它是否应该更新UI。
  2. 这些渲染函数或函数组件中的所有代码都将再次执行。

第1点可以说不是那么重要,因为React能够非常有效地计算差异。危险在于您编写的代码在每次React渲染时都被重复执行。

在上面的例子中,我们只有一个小的组件树。 但是想象一下,如果每个节点有更多子节点会发生什么,而这些子节点又可能有子组件。后面我们将讨论如何去优化它。

想要看到重新渲染的效果吗?

React DevTools允许我们在Components -> View Settings -> Highlight updates中设置渲染时高亮更新。这将展示所有虚拟渲染。

如果我们想看原生重渲染,我们需要在Chrome DevTools中三个点的菜单-> More tools -> Rendering -> Paint flashing。

现在点击我们的应用程序,首先会高亮React重渲染,然后是原生渲染,我们将看到React对原生渲染进行了多少的优化。

一个Chrome的Paint Flashing选项示例。

React什么时候重新渲染?

上面我们看到了是什么导致了我们的UI重新绘制,但是从什么开始调用React的渲染函数呢?

每当组件的状态发生变化时,React都会调度一次渲染。

调度渲染并不意味着渲染过程会立即发生。 React将尝试为此找到最佳时机去执行这个操作。

当我们调用setState函数(在React Hook中,我们会使用useState)改变状态时意味着React会触发更新。这不仅意味着组件的渲染函数将被调用,而且_所有后续的子组件都将重新渲染,无论它们的props是否改变。_

如果我们的应用程序结构组织的不好,我们可能会运行比预期更多的JavaScript,因为更新父节点意味着运行所有子节点的渲染功能。

在文章的最后一部分,我们将看到一些提示,可以帮助我们防止这种开销。

为什么在props改变时我的React组件不更新?

有两种情况,即使组件的props发生了变化,React也可能不会更新它:

  1. props并没有使用setState进行更新。
  2. prop的引用并没有发生变化。

正如我们之前看到的,当我们调用setState函数来更改状态(或函数组件中useStateHook提供的函数)时,React会重新渲染组件。

因此,子组件仅在父组件的状态随着这些函数之一发生变化时才会更新。

不可以直接改变props对象,因为这不会触发任何更改,并且React不会注意到这些更改。

//不要这样做
this.props.user.name = 'Felix';

我们需要改变父组件中的状态,而不是像这样改变props 。

const Parent = () => {
  const [user, setUser] = React.useState({ name: 'Felix' });
  const handleInput = (e) => {
    e.preventDefault();
    setUser({
      ...user,
      name: e.target.value,
    });
  };

  return (
    <>
      <input onChange={handleInput} value={user.name} />
      <Child user={user} />
    </>
  );
};

const Child = ({ user }) => (
   <h1>{user.name}</h1>
);

使用相应的React函数更改状态是非常重要。

请注意我们如何使用setUser更新状态,这是我们使用React.useState产生的函数。在类组件中和这个等效的是this.setState

强制React组件重新渲染

在我专注地使用React的两年中,我从来没有遇到过需要强制重新渲染的地步。 如果这才是你阅读这篇文章的目的,我建议你从头开始阅读这篇文章,因为通常有更好的方法来处理未更新的React组件。

但是,如果你绝对需要强制更新,则可以使用以下方法执行此操作:

使用React的forceUpdate函数

这个是最显而易见的。 在React类组件中,我们可以通过调用此函数来强制重新渲染: javascript this.forceUpdate();

使用React Hooks进行强制渲染

在React Hooks中,forceUpdate函数并不存在。但是我们可以使用React.useState来模拟一个不改变组件状态的forceUpdate操作:

//我想,我们是没机会使用它
const [state, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);

如何优化重新渲染

低效重新渲染的一个例子是父组件控制子组件的状态。 请记住:当组件的状态发生变化时,所有子组件都将重新渲染。

我将介绍React.memo中的例子做了一些扩展,添加了更多的嵌套子组件。让我们开始吧。

黄色的数字是计算每个组件的渲染函数已经执行的次数:

尽管我们只更新了蓝色组件的状态,但已经触发了更多其他组件的渲染。

控制组件何时更新

React 为我们提供了一些函数来防止这些不必要的更新。

让我们来看看它们,在此之后,我将向你展示另一种更有效的提高渲染性能的方法。

React.memo

首先,我们必须说一下之前提到的React.memo。我已经写了一个更深入介绍它的文章,但是总而言之,它是一个函数。它的功能是_当props没有改变时,阻止你的React Hook组件渲染。_

我们对前面的例子做了如下的修改

const TileMemo = React.memo(({ children }) => {
  let updates = React.useRef(0);
  return (
    <div className="black-tile">
      Memo
      <Updates updates={updates.current++} />
      {children}
    </div>
  );
});

在生产中使用它之前,你还需要了解一些关于此的信息。 我强烈建议你观看我另一篇文章介绍React.memo

这相当于在React的类组件上使用了React.PureCoponent

shouldComponentUpdate

这个函数是React的生命周期函数之一,它允许我们通过告诉 React 何时更新类组件来优化渲染性能。

它的参数是组件要进行渲染时,下一个props和下一个state:

shouldComponentUpdate(nextProps, nextState) {
  // return true or false
}

这个函数非常简单:返回true会让React调用渲染函数,返回false就会阻止React调用渲染函数。

设置key属性

在React中,我们会经常这么做。但是请找出这里面有什么问题。

<div>
  {
    events.map(event =>
      <Event event={event} />
    )
  }
</div>

这里,我们忘记了设置key属性。绝大部分的linters会警告你,但是它为什么这么重要呢?

在一些场景中,React非常依赖key属性去区分组件并进行性能优化。

在上面的例子中,如果一个事件被添加到数组的开头,React会认为第一个和所有后续元素都发生了变化,并会触发这些元素的重新渲染。 我们可以通过向元素添加一个键来防止这种情况:

<div>
  {
    events.map(event =>
      <Event event={event} key={event.id} />
    )
  }
</div>

尽量避免使用数组的索引作为key,尽可能用使用一些可以标识内容的东西作为key。key需要在兄弟姐妹中是唯一的。

优化组件结构

改进重新渲染的更好方法是稍微重构我们的代码。

小心我们逻辑代码所在的位置。如果我们把所有东西都放在应用程序的根组件中,那么无论我们如何使用React.memo函数,我们都无法改变我们的性能问题。

如果我们把这些逻辑放在靠近数据使用的地方,我们甚至不需要React.memo

我们可以看看优化过的版本:

我们可以看到,即便发生了状更新,但是其它组件也不会重新渲染。

这里面唯一的变化就是,我将处理状态的代码放到了一个单独的组件中:

const InputSelfHandling = () => {
  const [text, setText] = React.useState('');
  return (
    <input
      value={text}
      placeholder="Write something"
      onChange={(e) => setText(e.target.value)}
    />
  );
};

如果我们需要在应用程序的其他部分使用状态,我们可以使用React Context或MobX和Redux等替代品。

我希望我能让你更好地理解React的渲染机制是如何工作的,以及你可以充分利用它来做些什么。 在本文中,我必须对该主题进行一些额外的研究,以更好地了解React渲染工作方式。

我打算写更多关于前端性能的文章,所以如果你想获得有关最新文章的通知,请在Twitter上关注我并订阅我的电子邮件列表。

如果你已经读到这里了,也许你还想看看我另一篇文章介绍React.memo,它更深入地解释了这些API,一些你可能遇到的常见陷阱,以及为什么你不应该总是使用React.memo。 谢谢阅读。

文中代码


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK