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已经为你进行了优化。
但是坏消息是: 所有左侧的红色点代表这些组件的渲染函数都被执行了。
执行这些渲染函数有两个缺点:
- React必须在每个组件上运行其差异算法,以检查它是否应该更新UI。
- 这些渲染函数或函数组件中的所有代码都将再次执行。
第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也可能不会更新它:
- props并没有使用
setState
进行更新。 - prop的引用并没有发生变化。
正如我们之前看到的,当我们调用setState
函数来更改状态(或函数组件中useState
Hook提供的函数)时,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
。 谢谢阅读。
附件