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

文章介绍了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更新

让我们看一个例子:

1
2
3
4
5
6
7
8
9
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不会注意到这些更改。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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操作:

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

如何优化重新渲染

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

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

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

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

控制组件何时更新

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

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

React.memo

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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:

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

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

设置key属性

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

1
2
3
4
5
6
7
<div>
  {
    events.map(event =>
      <Event event={event} />
    )
  }
</div>

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

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

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

1
2
3
4
5
6
7
<div>
  {
    events.map(event =>
      <Event event={event} key={event.id} />
    )
  }
</div>

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

优化组件结构

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

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

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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。 谢谢阅读。

附件

文中代码