When does React render your component? React 何时渲染你的组件?
#reactWhen and why does React render my component exactly?
React 何时以及为何会精确渲染我的组件?
This post has been translated into Korean
这篇文章已被翻译成韩语
This post is my version of Mark Erikson's essay A (Mostly) Complete Guide to React Rendering Behavior where I try to answer one of the most commonly asked questions in this React community – "when or why does React render my component?" – with a tiny amount of React source code walkthrough.
这篇文章是我对 Mark Erikson 的文章《React 渲染行为(几乎)完全指南》的版本,我试图通过少量 React 源码的解读,来回答 React 社区中最常被问到的问题之一——“React 何时或为何会渲染我的组件?”
Normally I am not a big fan of drilling down to the implementation details and you certainly don't need to know that in order to be productive in React. However, when it comes to understanding the rendering behaviour and rules for bailing out of re-renders, the React docs haven’t provided a thorough enough explanation to satisfy me. Therefore to adequately answer those questions, I had to peek into the source code. That being said, this is not going to be a post about hard-core source code walkthrough. If you are interested in that, here is a great series made by JSer that you should check out.
通常我并不热衷于深入探讨实现细节,而且你确实不需要了解这些细节就能在 React 中高效工作。然而,当涉及到理解渲染行为以及跳过重新渲染的规则时,React 文档提供的解释并不足以让我满意。因此,为了充分回答这些问题,我不得不窥探一下源代码。话虽如此,这篇文章并不会成为一篇硬核的源代码解析。如果你对此感兴趣,这里有一个由 JSer 制作的精彩系列,你应该去了解一下。
Tl;dr 太长不看#
- React (re)renders your component when:
React 在以下情况下会(重新)渲染你的组件:- there is a state update scheduled by your component
您的组件已安排状态更新- including updates scheduled by custom hooks your component consumes
包括由您的组件使用的自定义钩子安排的更新
- including updates scheduled by custom hooks your component consumes
- the parent component got rendered and your component doesn’t meet the criteria for bailing out on re-rendering, where all these four conditions have to be satisfied at the same time:
父组件已渲染,而你的组件不符合跳过重新渲染的条件,必须同时满足以下四个条件:- Your component has been rendered before i.e. it already mounted
您的组件之前已经渲染过,即它已经挂载 - No
props
(referentially) changed
没有props
(引用上)发生变化 - No any context value consumed by your component changed
您的组件消耗的任何上下文值均未发生变化 - Your component itself didn’t schedule an update
您的组件本身没有安排更新
- Your component has been rendered before i.e. it already mounted
- there is a state update scheduled by your component
- You probably shouldn’t need to worry about seemingly unnecessary re-renders until it becomes a performance issue. Check out the flow chart I made for solutions you can adopt when a performance issue occurs.
在性能问题出现之前,您可能无需担心看似不必要的重新渲染。请查看我制作的流程图,了解在性能问题发生时可以采取的解决方案。
Disclaimer: I haven't used React's concurrent mode so some parts of this post might not be applicable in concurrent React.
免责声明:我尚未使用过 React 的并发模式,因此本文中的某些部分可能不适用于并发 React。
What does the word “render” mean? “render”这个词是什么意思?#
I don’t know if you have noticed this – I kept saying “React renders your component”, as opposed to “your component renders”. People use them interchangeably. It is largely an arbitrary decision. However, call me pedantic but I do want to use the former exclusively in this post because it describes how React works more accurately. Your components – functions augmented by React with the ability to schedule an update on the UI – are called by React, not the other way around, regardless of whether that render was a result of your component proactively changing its own state or some other changes.
我不知道你是否注意到了这一点——我一直说“React 渲染你的组件”,而不是“你的组件渲染”。人们常常互换使用这两种说法。这很大程度上是一个随意的选择。不过,尽管可能显得吹毛求疵,但我确实想在这篇文章中专门使用前一种说法,因为它更准确地描述了 React 的工作原理。你的组件——那些被 React 赋予了调度 UI 更新能力的函数——是由 React 调用的,而不是反过来,无论这次渲染是由于你的组件主动改变了自己的状态,还是其他变化引起的。
As one of its core design principles, React has full control over scheduling and updating the UI. This means a few things to us:
作为其核心设计原则之一,React 完全掌控着 UI 的调度和更新。这对我们意味着以下几点:
- One state update made by our component doesn’t necessarily translate into one render (one invocation of your component by React) because:
我们组件的一次状态更新并不一定转化为一次渲染(React 对组件的一次调用),因为:- React might not think there are any meaningful changes to your component’s state (determined by
object.is
)
React 可能认为你的组件状态没有发生有意义的变化(由object.is
确定) - React tries to batch state updates into one render pass.
React 尝试将状态更新批量处理为一次渲染过程。- However, React cannot batch state updates in promises, because React has no control over when they are resolved, same thing with native event handlers,
setTimeout
,setInterval
, andrequestAnimationFrame
, all of which are running much later in a totally separate event loop call stack
然而,React 无法在 Promise 中批量处理状态更新,因为 React 无法控制它们的解决时机,原生事件处理器也是如此,setTimeout
、setInterval
和requestAnimationFrame
,它们都在完全独立的事件循环调用栈中运行,时间上要晚得多
- However, React cannot batch state updates in promises, because React has no control over when they are resolved, same thing with native event handlers,
- React might split the work in chunks across different render passes (a concurrent React feature)
React 可能会将工作拆分成多个块,分布在不同的渲染阶段(一个并发 React 特性)
- React might not think there are any meaningful changes to your component’s state (determined by
- One render to your component doesn’t necessarily translate into a visual update on the UI because React could decide to render your component (i.e. call your function) for a variety of reasons.
一次对组件的渲染并不一定意味着 UI 上的视觉更新,因为 React 可能出于多种原因决定渲染你的组件(即调用你的函数)。
In React 17, some state updates cannot be auto-batched...
在 React 17 中,某些状态更新无法自动批处理...
In React 17, some state updates cannot be batched, such as updates in promises, because React has no control over when they are resolved, same thing with native event handlers, setTimeout
, setInterval
, and requestAnimationFrame
, all of which are running much later in a totally separate event loop call stack
在 React 17 中,某些状态更新无法批量处理,例如在 Promise 中的更新,因为 React 无法控制它们何时被解决,原生事件处理程序也是如此, setTimeout
、 setInterval
和 requestAnimationFrame
,它们都在完全独立的事件循环调用栈中运行,时间要晚得多
In React 18, all state updates can be auto-batched.
在 React 18 中,所有状态更新都可以自动批处理。
However, having React in control of rendering doesn’t mean you shouldn’t care about when or why it decides to render your component. We can’t rely on React to have us back. Understanding the underlying mechanism React uses to render your components comes in handy when we face performance issues.
然而,由 React 控制渲染并不意味着你不应该关心它何时或为何决定渲染你的组件。我们不能完全依赖 React 来为我们兜底。当我们面临性能问题时,理解 React 用于渲染组件的底层机制会非常有用。
Let's also define what "update" means in different contexts...
让我们也定义一下“更新”在不同上下文中的含义...
Alongside the word “render”, the word “update” is going to be used a lot. It means different things in different contexts.
除了“渲染”这个词,“更新”这个词也会经常使用。它在不同的上下文中有不同的含义。
When used in “your component schedules an update”, it means the component wants to change its own state and ask React to reflect that change on the UI. Here the update is the reason that React would render (call) your component. Note that at the end whether React decides to render your component, how many times React decides to render your component and in how long a delay it decides to render depends on a variety of factors.
当用于“你的组件安排更新”时,意味着组件希望改变自身状态,并要求 React 在 UI 上反映这一变化。这里的更新是 React 会渲染(调用)你的组件的原因。需要注意的是,最终 React 是否决定渲染你的组件、决定渲染多少次以及延迟多久渲染,取决于多种因素。
When used in “React makes an update to the UI”, it means it either React mutates an existing DOM node or creates a new DON node to match its internal representation of the DOM tree. Here the update is the result of rendering your components
当在“React 对 UI 进行更新”中使用时,意味着 React 要么修改现有的 DOM 节点,要么创建一个新的 DOM 节点以匹配其内部对 DOM 树的表示。这里的更新是渲染组件的结果。
So when does React render your component exactly? 那么,React 究竟何时渲染你的组件呢?#
There are two types of rendering that can happen to your component:
您的组件可能会发生两种类型的渲染:
- proactive rendering: 主动渲染:
- Your component (or the custom hooks it consumes) proactively schedules updates to change its own state.
您的组件(或其使用的自定义钩子)主动安排更新以更改其自身状态。 - You call
ReactDOM.render
directly. 你直接调用ReactDOM.render
。
- Your component (or the custom hooks it consumes) proactively schedules updates to change its own state.
- passive rendering: The parent component(s) schedule state updates and your component doesn’t meet the bail-out criteria.
被动渲染:父组件调度状态更新,而你的组件不符合跳过渲染的条件。
Proactive rendering 主动渲染#
"proactive rendering" is a made-up word by me. By "proactive", I mean the component itself (or the custom hooks it uses) proactively makes changes to its own state to schedule updates via:
“主动渲染”是我自创的一个词。所谓“主动”,指的是组件本身(或其使用的自定义钩子)主动改变自身状态,通过以下方式安排更新:
- Component.prototype.setState (i.e.
this.setState
) if it is a class components.
Component.prototype.setState(即this.setState
)如果它是一个类组件。 - dispatchAction exposed by Hooks if it is a function components:
如果是一个函数组件,由 Hooks 暴露的 dispatchAction:- Both the
dispatch
function from theuseReducer
Hook and the state updater function from theuseState
Hook usedispatchAction
underlying.
来自useReducer
Hook 的dispatch
函数和来自useState
Hook 的状态更新函数都使用了dispatchAction
作为底层实现。
- Both the
Another way to proactively schedule an update is to call ReactDOM.render
directly. Here is an example in React official docs:
另一种主动调度更新的方法是直接调用 ` ReactDOM.render
`。以下是 React 官方文档中的一个示例:
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);
More implementation details for the rendering phase...
Regardless of which exact function you used to schedule the update, all the them use the scheduleUpdateOnFiber
in the reconciler, which–you can probably tell by its self-explanatory name–schedules updates on Fiber.
无论你使用哪个具体函数来安排更新,它们都使用了协调器中的 scheduleUpdateOnFiber
,从其自解释的名称可以看出,它会在 Fiber 上安排更新。
But what is a Fiber? Fiber was introduced in React 16. It is the new reconciliation algorithm and also a new data structure to present a unit of work internal to React. A fiber node is created from a ReactElement
by the reconciler. Normally every ReactElement
has a corresponding fiber node but there are some exceptions. For example, a Fragment
type of ReactElement doesn’t have a corresponding fiber node.
但什么是 Fiber 呢?Fiber 是在 React 16 中引入的。它是一种新的协调算法,也是一种新的数据结构,用于表示 React 内部的工作单元。协调器会从 ReactElement
创建一个 fiber 节点。通常每个 ReactElement
都有一个对应的 fiber 节点,但也有一些例外。例如, Fragment
类型的 ReactElement 就没有对应的 fiber 节点。
One major distinction between a fiber node and a ReactElement
is that a ReactElement
is immutable, getting re-created all the time while a fiber node is mutable and can be reused. When React bails out on rendering a component, it reuses its current corresponding fiber node in the fiber tree it constructs as opposed to create a new one.
纤维节点与 ReactElement
之间的一个主要区别在于, ReactElement
是不可变的,总是被重新创建,而纤维节点是可变的,可以重复使用。当 React 决定跳过某个组件的渲染时,它会重用当前对应的纤维树中的纤维节点,而不是创建一个新的。
This is not a post about React internals. You can check out this article if you want to learn more about fiber nodes and the whole reconciliation process.
这不是一篇关于 React 内部机制的文章。如果你想了解更多关于 fiber 节点和整个协调过程的内容,可以查看这篇文章。
Passive rendering 被动渲染#
Passive rendering happens to your component because React rendered some parent component(s) and your component does not meet the bail-out criteria.
被动渲染发生在你的组件上,因为 React 渲染了某些父组件,而你的组件不符合跳过渲染的条件。
function Parent() {
return (
<div>
<Child />
</div>
);
}
In the example above, if Parent
gets rendered by React, Child
also gets rendered even though its props have no meaningful changes other than that its reference/identity changed. (More on this later)
在上面的例子中,如果 Parent
被 React 渲染, Child
也会被渲染,即使它的 props 除了引用/标识发生变化外,没有其他有意义的更改。(稍后会详细讨论这一点)
During the render phase, React recursively traverses down the component tree to render your components. As a result, if Child
has other children components, they would get rendered too (again, if they don’t meet the bail-out criteria)
在渲染阶段,React 会递归遍历组件树以渲染你的组件。因此,如果 Child
有其他子组件,它们也会被渲染(同样,如果它们不符合跳过渲染的条件)。
function Child() {
return <GrandChild /> // if `Child` gets rendered, `GrandChild` is rendered too
}
However, if one of these components meets the bail-out criteria, React will not render that component.
然而,如果其中一个组件符合保释标准,React 将不会渲染该组件。
The next logical question is, what are the bail-out criteria?
下一个合乎逻辑的问题是,退出标准是什么?
To answer that, let’s us take a look at two examples.
要回答这个问题,让我们来看两个例子。
Not every child component is created equal 并非所有子组件都是平等的#
Let’s first a look at an example:
让我们先来看一个例子:
default function App() {
return (
<Parent lastChild={<ChildC />}>
<ChildB />
</Parent>
);
}
function Parent({ children, lastChild }) {
return (
<div className="parent">
<ChildA />
{children}
{lastChild}
</div>
);
}
function ChildA() {
return <div className="childA"></div>;
}
function ChildB() {
return <div className="childB"></div>;
}
function ChildC() {
return <div className="childC"></div>;
}
If Parent
schedules an update, which component(s) will get re-rendered?
如果 Parent
安排了更新,哪些组件将会被重新渲染?
Unsurprisingly, Parent
itself will get re-rendered by React since it is the component that schedules the update. But will all the children ChildA
, ChildB
and ChildC
get re-rendered as well?
不出所料, Parent
本身会被 React 重新渲染,因为它是调度更新的组件。但是所有的子组件 ChildA
、 ChildB
和 ChildC
也会被重新渲染吗?
To answer that question, I prepared a Hook called useForceRender
to schedule re-renders at some interval via setInterval
为了回答这个问题,我准备了一个名为 useForceRender
的 Hook,通过 setInterval
以一定间隔安排重新渲染
function useForceRender(interval) {
const render = useReducer(() => ({}))[1];
useEffect(() => {
const id = setInterval(render, interval);
return () => clearInterval(id);
}, [interval]);
}
I use it inside Parent
and see which child gets re-rendered:
我在 Parent
中使用它,并查看哪个子组件被重新渲染:
function Parent({ children, lastChild }) {
useForceRender(2000);
console.log("Parent is rendered");
return (
<div className="parent">
<ChildA />
{children}
{lastChild}
</div>
);
}
Try it on codesandbox 在 CodeSandbox 上试试

ChildA
got re-rendered, which shouldn’t be surprising to us since we know that its parent scheduled the updates and got re-rendered as a result.
ChildA
被重新渲染了,这对我们来说并不意外,因为我们知道它的父组件安排了更新并因此被重新渲染。
However, unlike ChildA
, ChildB
and ChildC
didn’t get re-rendered. Because ChildB
and ChildC
met the bail-out criteria, so React skipped rendering them.
然而,与 ChildA
不同, ChildB
和 ChildC
并未被重新渲染。因为 ChildB
和 ChildC
符合跳过渲染的条件,所以 React 跳过了它们的渲染。
This might not be news to you. Kent C. Dodds, Dan Abramov and Mark all have written about this optimization technique in their blog posts.
这对你来说可能不是新闻。Kent C. Dodds、Dan Abramov 和 Mark 都在他们的博客文章中写过这种优化技术。
Context consumers get rendered whenever the provider get rendered 每当提供者渲染时,上下文消费者也会被渲染#
Passive rendering can also happen when your component is a context consumer.
当你的组件是上下文消费者时,也可能发生被动渲染。
Let’s tweak our previous example to make Parent
a context provider and ChildC
a context consumer.
让我们调整之前的示例,使 Parent
成为上下文提供者, ChildC
成为上下文消费者。
const Context = createContext();
export default function App() {
return (
<Parent lastChild={<ChildC />}>
<ChildB />
</Parent>
);
}
function Parent({ children, lastChild }) {
useForceRender(2000);
const contextValue = {};
console.log("Parent is rendered");
return (
<div className="parent">
<Context.Provider value={contextValue}>
<ChildA />
{children}
{lastChild}
</Context.Provider>
</div>
);
}
function ChildA() {
console.log("ChildA is rendered");
return <div className="childA"></div>;
}
function ChildB() {
console.log("ChildB is rendered");
return <div className="childB"></div>;
}
function ChildC() {
console.log("ChildC is rendered");
const value = useContext(Context)
return <div className="childC"></div>;
}
Try it on codesandbox 在 CodeSandbox 上试试
Here is the result:
结果如下:

Every time Parent
get rendered (called) by React, it creates a new contextValue
, which is different referentially compared to the previous contextValue
. As a result, the context consumer ChildC
gets a different context value, and React go ahead and re-render ChildC
to reflect that change.
每当 Parent
被 React 渲染(调用)时,它都会创建一个新的 contextValue
,这个新的 contextValue
在引用上与之前的 contextValue
不同。因此,上下文消费者 ChildC
会接收到一个不同的上下文值,React 会继续重新渲染 ChildC
以反映这一变化。
Note that if contextVlaue
were a primitive value, e.g. number or string, then its equality wouldn’t change between re-renders, and as a result, ChildC wouldn't get re-rendered.
请注意,如果 contextVlaue
是一个原始值,例如数字或字符串,那么它在重新渲染之间的相等性不会改变,因此 ChildC 不会重新渲染。
Note that bail-out is on individual-component level
请注意,保释是在单个组件级别上进行的
If one of these components meets the bail-out criteria, React will not render that component. However, React would still proceed to check if there are any updates needed for the children of the bailed-out component though. In the example below, while ChildA
and ChildB
get bailed out, their descendent – ChildC
– still gets re-rendered whenever Parent
gets re-rendered.
function useForceRender(interval) {
const render = useReducer(() => ({}))[1]
useEffect(() => {
const id = setInterval(render, interval)
return () => clearInterval(id)
}, [interval])
}
function App() {
return (
<Parent>
<ChildA />
</Parent>
)
}
function Parent({ children }) {
useForceRender(1000)
const contextValue = {}
console.log('Parent is rendered')
return (
<div className="parent">
<Context.Provider value={contextValue}>{children}</Context.Provider>
</div>
)
}
function ChildA() {
console.log('ChildA is rendered')
return (
<div className="childA">
<ChildB />
</div>
)
}
function ChildB() {
console.log('ChildB is rendered')
return (
<div className="childB">
<ChildC />
</div>
)
}
function ChildC() {
console.log('ChildC is rendered')
const value = useContext(Context)
return <div className="childC"></div>
}

Bail-out criteria 紧急退出标准#
We have seen enough examples, so let’s talk about what the bail-out criteria really are.
我们已经看够了例子,现在来谈谈真正的保释标准是什么。
To get the ground truth, we have to dig into the source. But where do we even start?
要获取真实情况,我们必须深入源头。但我们该从何处着手呢?
We can profile the app in the performance tab to see the call stack during the runtime.
我们可以在性能选项卡中对应用程序进行分析,以查看运行时的调用堆栈。

The above screenshot is a snapshot of the call stack when our App
first mounted.
上述截图是我们 App
首次挂载时的调用堆栈快照。
Our app was mounted by ReactDOM.render
, resulting an update scheduled via scheduleUpdateOnFiber
. This is the entry point for React to update fiber nodes, regardless if React renders the component for the first time or not.
我们的应用程序由 ReactDOM.render
挂载,导致通过 scheduleUpdateOnFiber
安排了更新。这是 React 更新 fiber 节点的入口点,无论 React 是否是首次渲染该组件。
There are too many details involved but a pattern we can recognize is that for every component React renders, it needs to call beginWork
. It seems like the place where the secrete about the bail-out behaviour would lie.
涉及的细节太多,但我们可以识别出一个模式:对于 React 渲染的每个组件,它都需要调用 beginWork
。似乎关于跳过行为(bail-out)的秘密就藏在这里。
Let’s take a look at its source code:
让我们来看一下它的源代码:
It is a long function. It accepts three arguments: current
, workInProgress
and renderLanes
. Current
is a pointer to the existing fiber node, workInProgress
is a pointer to the new fiber node being constructed to reflect the update. Why are there two fiber nodes involved for each update? This is called double buffering, an optimization technique to improve perceived performance.
这是一个较长的函数。它接受三个参数: current
、 workInProgress
和 renderLanes
。 Current
是指向现有 fiber 节点的指针, workInProgress
是指向正在构建以反映更新的新 fiber 节点的指针。为什么每次更新都涉及两个 fiber 节点?这被称为双缓冲,是一种优化技术,旨在提高感知性能。
Although there is a lot going on in this function (as well as in this file), it is not hard to find where exactly React enters the bail-out logic:
尽管这个函数(以及这个文件中)有很多内容,但找到 React 进入跳过逻辑的确切位置并不难:
// ...omitted for brevity
// No pending updates or context. Bail out now.
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
In order for us to reach this line, these conditions have to be satisfied:
为了让我们能够到达这一行,必须满足以下条件:
current !== null
oldProps === newProps
hasLegacyContextChanged() === false
hasScheduledUpdateOrContext === false
This roughly translates to:
这大致翻译为:
- Your component has been rendered before i.e. it already mounted
您的组件之前已经渲染过,即它已经挂载 - No
props
changed 没有props
更改 - No any context value consumed by your component changed
您的组件消耗的任何上下文值均未发生变化 - Your component itself didn’t schedule an update
您的组件本身没有安排更新
Rule 1 and rule 4 are easy to understand.
规则 1 和规则 4 很容易理解。
Let’s focus on Rule 2 and Rule 3.
让我们专注于规则 2 和规则 3。
How to not change props 如何不改变 props#
A component’s props
is a property of its corresponding ReactElement
, created by React.createElement
. Because ReactElement
s are immutable, every time React renders (calls) your component, React.createElement
is called to produce a new ReactElement
. As a result, your component's props
is created from scratch for every re-render.
组件的 props
是其对应 ReactElement
的一个属性,由 React.createElement
创建。由于 ReactElement
是不可变的,每次 React 渲染(调用)你的组件时,都会调用 React.createElement
来生成一个新的 ReactElement
。因此,每次重新渲染时,你的组件的 props
都会从头开始创建。
Look back on our first example:
回顾我们的第一个例子:
function Parent() {
return (
<div>
<Child />
</div>
);
}
The <Child />
returned from Parent
gets compiled to React.createElement(Child, null)
by Babel, and that creates a ReactElement
of this shape {type: Child, props: {}}
从 Parent
返回的 <Child />
被 Babel 编译为 React.createElement(Child, null)
,从而创建了具有这种形状 {type: Child, props: {}}
的 ReactElement
Since props
is an JavaScript object, so its reference changes every time it gets re-created. By default, React uses ===
to compare the previous props
and the current props
. As a result, the props
are considered different between re-renders. That’s why even though Child
receives nothing from Parent
as part of its props
, it still gets re-rendered whenever Parent
gets re-rendered – React.createElement
is called for Child
and that creates a new props
object.
由于 props
是一个 JavaScript 对象,因此每次重新创建时,其引用都会改变。默认情况下,React 使用 ===
来比较先前的 props
和当前的 props
。因此,在重新渲染之间, props
被视为不同。这就是为什么即使 Child
没有从 Parent
接收到任何内容作为其 props
的一部分,每当 Parent
重新渲染时,它仍然会被重新渲染—— React.createElement
被调用以处理 Child
,这会创建一个新的 props
对象。
However, if we can lift Child
up and pass it down via Parent
’s props
:
然而,如果我们能够通过 Parent
的 props
将 Child
提升并传递下去:
function App() {
return <Parent><Child /></Parent>
}
function Parent({children}) {
return (
<div>
{children}
</div>
);
}
Then whenever Parent
gets rendered by React, there is no React.createElement
function call for Child
. As a result, no new props
created for Child
, and that makes it meet all all four bail-out rules I mentioned above.
每当 Parent
被 React 渲染时,就不会有 React.createElement
函数调用 Child
。因此,不会为 Child
创建新的 props
,这使得它符合我上面提到的所有四个跳过规则。
This is why in this example, only ChildA
gets re-rendered whenever Parent
schedules an update:
这就是为什么在这个例子中,每当 Parent
调度更新时,只有 ChildA
会被重新渲染:
function Parent({ children, lastChild }) {
return (
<div className="parent">
<ChildA /> // only ChildA gets re-rendered
{children} // bailed out
{lastChild} // bailed out
</div>
);
}
How to change the rule React uses to detect props changes 如何更改 React 用于检测 props 更改的规则#
I mentioned that, by default, React uses ===
to compare the previous props
and the current props
.
我提到过,默认情况下,React 使用 ===
来比较之前的 props
和当前的 props
。
Luckily, React provides an alternative way to detect props
change if we make our component a PureComponent
or wrap it in React.memo
. In those cases, instead of using ===
to check if the reference changed, React would shallow compare every property in the props
, conceptually similar to Object.keys(prevProps).some(key => prevProps[key] !== nextProps[key])
.
幸运的是,如果我们使组件成为 PureComponent
或将其包装在 React.memo
中,React 提供了一种替代方法来检测 props
的变化。在这些情况下,React 不会使用 ===
来检查引用是否改变,而是会浅比较 props
中的每个属性,概念上类似于 Object.keys(prevProps).some(key => prevProps[key] !== nextProps[key])
。
However such optimization should not be abused and there are reasons why React didn’t make it the default rendering behaviour. Dan Abramov has repeatedly pointed out that we should not ignore the costs of comparing
props
and a lot of times there are better alternatives.
然而,这种优化不应被滥用,React 没有将其设为默认渲染行为是有原因的。Dan Abramov 多次指出,我们不应忽视比较 `props
` 的成本,很多时候存在更好的替代方案。
How to not change context values 如何不改变上下文值#
If your component is a consumer of some context value, then when the provider gets re-rendered and the context value is changed (even only referentially), your component gets re-rendered too.
如果你的组件是某个上下文值的消费者,那么当提供者重新渲染且上下文值发生变化时(即使只是引用上的变化),你的组件也会重新渲染。
This is why in this example, the context consumer ChildC
gets re-rendered whenever Parent
gets re-rendered:
这就是为什么在这个例子中,每当 Parent
重新渲染时,上下文消费者 ChildC
也会重新渲染:
const Context = createContext();
export default function App() {
return (
<Parent lastChild={<ChildC />}>
<ChildB />
</Parent>
);
}
function Parent({ children, lastChild }) {
useForceRender(2000);
const contextValue = {};
console.log("Parent is rendered");
return (
<div className="parent">
<Context.Provider value={contextValue}>
<ChildA />
{children}
{lastChild}
</Context.Provider>
</div>
);
}
function ChildC() {
console.log("ChildC is rendered");
const value = useContext(Context)
return <div className="childC"></div>;
}
Note this is not bad per se. The compound component pattern relies on this exact rendering behaviour of context consumers. However it can become a performance issue when a provider has too many consumers or consumers that are too expensive to get re-rendered unnecessarily.
注意,这本身并不是坏事。复合组件模式正是依赖于上下文消费者的这种渲染行为。然而,当提供者有太多消费者,或者消费者重新渲染的成本过高时,这可能会成为性能问题。
In which case, the easiest fix is to wrap your non-primitive context values into useMemo
so they stay referentially the same between re-renders of the provider component:
在这种情况下,最简单的修复方法是将非原始上下文值包装到 ` useMemo
` 中,以便它们在提供者组件重新渲染之间保持引用相同:
function Parent({ children, lastChild }) {
const contextValue = {};
const memoizedCxtValue = useMemo(contextValue)
return (
<div className="parent">
<Context.Provider value={memoizedCxtValue}>
<ChildA />
{children}
{lastChild}
</Context.Provider>
</div>
);
}
There is one exception where you don't want to use useMemo
to wrap context values
有一种例外情况,您不希望使用 useMemo
来包裹上下文值
You can wrap context values inside useMemo
as a performance optimization technique if the consumer subtree can be huge.
However there is one exception to this – if the context provider component is at the top of the component tree, then there is no point in memoizing the context value – as no passive rendering can happen to it. For example:
const ContextA = createContext(null);
const Parent = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = useMemo(() => [state, dispatch], [state]);
return (
<ContextA.Provider value={value}>
<Child1 />
</ContextA.Provider>
);
};
If Parent
is at the top of the component tree, i.e. it doesn’t have any other parent components, then the only reason React would re-render it is dispatch
is called, in which case the memoization we applied via useMemo
would be busted anyway and as a result the subtree is re-rendered. So we are better off passing down the value directly, as in:
const ContextA = createContext(null);
const Parent = () => {
const [state, dispatch] = useReducer(reducerA, initialStateA);
return (
<ContextA.Provider value={[state, dispatch]}>
<Child1 />
</ContextA.Provider>
);
};
There are many other techniques you can employ to optimize context consumption. My friend Vladimir has a great post on this that you should check out.
还有许多其他技术可以用来优化上下文消耗。我的朋友 Vladimir 有一篇关于这个主题的精彩文章,你应该去看看。
It is all based on one implicit premise... 这一切都基于一个隐含的前提...#
My confession to you is that the whole bail-out thing is based on the premise that your component is always rendered in the same place in the component tree. The reason I didn’t state that upfront is because normally that is the case. However if you:
我对你的坦白是,整个保释机制的前提是你的组件总是在组件树的同一位置渲染。我之所以没有一开始就说明这一点,是因为通常情况下确实如此。然而,如果你:
- switch between different component types at the same position
在同一位置切换不同的组件类型 - render the same component at the different position
在不同位置渲染相同的组件 - deliberately change its
key
故意更改其key
...then react will destroy the entire subtree and re-build it from scratch. Not only will your component get re-rendered, but also its state will be lost.
...然后 React 将销毁整个子树并从头开始重新构建。不仅你的组件会重新渲染,而且它的状态也会丢失。
Check out the Preserving and Resetting State from new React docs to learn more about this.
查看新的 React 文档中的《保留和重置状态》以了解更多信息。
What’s the moral? 寓意何在?#
Whether you found the bail-out rules complex or not, one simple idea I would like you to walk away with is that – React could re-render your component for a variety of reasons.
无论你是否觉得这些救援规则复杂,我希望你能记住一个简单的观点——React 可能会因为各种原因重新渲染你的组件。
It is necessary for React to do so because two of the hardest problems in UI engineering is to avoid inconsistency and staleness in your app’s states.
React 必须这样做,因为在 UI 工程中,最困难的两个问题是避免应用程序状态的不一致和过时。
Therefore, make sure your component is ready for re-renders and be resilient to a lot of them. You can stress-test your component with the hook I made (an idea I stole from Dan Abramov). Furthermore, make your component idempotent so rendering your component one time or multiple times shouldn’t cause any differences on the actual UI (except for the performance drop it incurred).
因此,请确保您的组件已准备好进行重新渲染,并且能够应对大量的重新渲染。您可以使用我制作的钩子(这个想法是从 Dan Abramov 那里借鉴来的)对您的组件进行压力测试。此外,使您的组件具有幂等性,这样无论渲染一次还是多次,都不应对实际 UI 造成任何差异(除了性能下降之外)。
However, when excessive re-rendering causes two other hardest problems in UI engineering – responsiveness and latency, hopefully you already know where to investigate and how to optimize.
然而,当过多的重新渲染导致 UI 工程中另外两个最棘手的问题——响应性和延迟时,希望你已经知道该从哪里入手调查以及如何优化。
The flow chart 流程图#
I made a flow chart that might be helpful for you to check for unexpected re-render:
我制作了一个流程图,可能有助于你检查意外的重新渲染:
