(大部分)完整的渲染行为反应指南

2020-05-17 08:15:49

我的实用教程系列现在是一门关于教育的互动课程。有关更多详细信息,请查看课程公告帖子。

有关Reaction渲染行为的详细信息,以及上下文和Reaction-Redux的使用如何影响渲染的详细信息。

我已经看到很多关于Reaction何时、为什么以及如何重新呈现组件的持续困惑,以及上下文和Reaction-Redux的使用将如何影响这些重新呈现的时间和范围。在打了几十次这个解释的变体之后,似乎值得试着写一份综合的解释,我可以让人们参考一下。请注意,所有这些信息都已经在网上提供,并在许多其他优秀的博客帖子和文章中进行了解释,其中几篇我在更多信息部分的末尾链接以供参考。但是,人们似乎很难把这些碎片拼凑在一起,以便完全理解,所以希望这能帮助某人澄清一些事情。

呈现是根据当前道具和状态的组合,要求您的组件描述他们想要的UI部分现在看起来是什么样子的过程。

在呈现过程中,Reaction将从组件树的根开始向下循环,以查找已标记为需要更新的所有组件。对于每个标记的组件,Reaction将调用classComponentInstance.Render()(对于类组件)或FunctionComponent()(对于函数组件),并保存呈现输出。

组件的呈现输出通常以JSX语法编写,然后在编译和准备部署JS时将其转换为React.createElement()调用。createElement返回Reaction元素,这些元素是描述UI预期结构的纯JS对象。示例:

//此JSX语法:return<;SomeComponent a={42}b=";Testing";>;Text here<;/SomeComponent>;//转换为此调用:return React.createElement(SomeComponent,{a:42,b:";Testing";},";Text here";)//这将成为此元素对象:{type:Some34;此处文字";]}。

在从整个组件树收集呈现输出之后,React将区分新的对象树(通常称为虚拟DOM),并收集需要应用的所有更改的列表,以使实际DOM看起来像当前所需的输出。差额和计算过程称为对账。

React在提交阶段更新了DOM之后,它将同步运行ComponentDidMount和ComponentDidUpdate类生命周期方法,以及useLayoutEffect挂钩。

然后,Reaction设置一个短暂的超时,当超时到期时,运行所有的useEffect挂钩。

您可以在这个出色的Reaction生命周期方法图中看到类生命周期方法的可视化。(它目前没有显示效果挂钩的时间,这是我希望添加的内容。)。

在React即将推出的并发模式中,它能够在呈现阶段暂停工作,以允许浏览器处理事件。Reaction稍后将根据需要恢复、丢弃或重新计算该工作。渲染过程完成后,Reaction仍将在一个步骤中同步运行提交阶段。

要理解这一点的一个关键部分是,呈现与更新DOM不是一回事,呈现组件时可能不会发生任何可见的更改。React渲染组件时:

该组件可能会返回与上次相同的渲染输出,因此不需要更改。

在并发模式下,Reaction可能会多次呈现一个组件,但如果其他更新使当前正在进行的工作无效,则每次都会丢弃呈现输出。

初始渲染完成后,有几种不同的方法可以告诉Reaction将重新渲染排队:

Reaction的默认行为是,当父组件呈现时,Reaction将递归呈现其内部的所有子组件!

例如,假设我们有一个A>;B>;C>;D的组件树,并且我们已经在页面上显示了它们。用户单击B中递增计数器的按钮:

Reaction发现A没有被标记为需要更新,并跳过它。

Reaction发现B被标记为需要更新,并呈现它。B像上次一样退还<;C/>;。

C最初没有被标记为需要更新。但是,因为其父B已渲染,所以Reaction现在向下移动并渲染C。C再次返回<;D/>;。

D也未标记为渲染,但由于其父C已渲染,所以Reaction也会向下移动并渲染D。

默认情况下,呈现组件将导致其内部的所有组件也被呈现!

在正常渲染中,Reaction不关心道具是否更改-它会因为父组件渲染而无条件渲染子组件!

这意味着在您的根<;App>;组件中调用setState()(没有任何其他更改改变行为)将导致Reaction重新呈现组件树中的每个组件。毕竟,React最初的销售宣传之一是,表现得像我们在每次更新时都会重新绘制整个应用程序。

现在,树中的大多数组件很可能会返回与上次完全相同的呈现输出,因此Reaction不需要对DOM进行任何更改。但是,Reaction仍然需要要求组件呈现自身,并对呈现输出进行差异化处理。这两件事都需要时间和精力。

请记住,呈现并不是一件坏事-它的反应方式知道它是否需要真正对DOM进行任何更改!

话虽如此,渲染工作有时也会白费力气,这也是事实。如果组件呈现输出没有更改,且DOM的该部分不需要更新,那么呈现该组件的工作确实有点浪费时间。

Reaction组件渲染输出应始终完全基于当前道具和当前组件状态。因此,如果我们提前知道组件的道具和状态没有改变,我们也应该知道呈现输出是相同的,该组件不需要改变,并且我们可以安全地跳过呈现它的工作。

通常,当试图提高软件性能时,有两种基本方法:1)更快地完成相同的工作,2)减少工作。优化Reaction渲染主要是通过在适当的时候跳过渲染组件来减少工作。

React.Component.shouldComponentUpdate:一个可选的类组件生命周期方法,将在呈现过程的早期调用。如果返回False,Reaction将跳过呈现组件。它可能包含要用于计算布尔结果任何逻辑,但最常用的方法是检查组件的属性和状态自上次以来是否已更改,如果它们未更改,则返回False。

React.PureComponent:由于属性和状态比较是实现shouldComponentUpdate的最常见方式,因此PureComponent基类默认情况下实现该行为,且可以用来代替Component+shouldComponentUpdate。

React.memo():内置的高阶组件类型。它接受您自己的组件类型作为参数,并返回一个新的包装组件。包装器组件的默认行为是检查是否有任何道具已更改,如果没有,则阻止重新渲染。函数组件和类组件都可以使用React.memo()包装。(可能会传入自定义比较回调,但它实际上只能比较旧道具和新道具,因此自定义比较回调的主要用例将仅比较特定道具字段,而不是所有道具字段。)。

所有这些方法都使用一种称为“浅等式”的比较技术。这意味着检查两个不同对象中的每个字段,并查看对象的任何内容是否具有不同的值。换句话说,obj1.a=obj2.a&;&;obj1.b=obj2.b&;&;.。这通常是一个快速的过程,因为=比较对于JS引擎来说非常简单。因此,这三种方法等同于Const shouldRender=!shallowEquity(newProps,prevProps)。

还有一种不太为人所知的技术:如果Reaction组件在其呈现输出中返回与上次完全相同的元素引用,Reaction将跳过重新呈现该特定的子级。

对于所有这些技术,跳过呈现组件意味着Reaction也会跳过呈现整个子树,因为它实际上设置了一个停止标志来停止默认的子级递归渲染子行为。

我们已经看到,默认情况下,Reaction会重新呈现所有嵌套的组件,即使它们的道具没有更改。这也意味着将新引用作为道具传递给子组件并不重要,因为无论您是否传递相同的道具,它都会呈现出来。所以,像这样的事情是完全可以的:

函数ParentComponent(){const onclick=()=>;{console.log(";Button click";)}const data={a:1,b:2}return<;NormalChildComponclick={onclick}data={data}/>;}。

每次呈现ParentComponent时,它都会创建一个新的onClick函数引用和一个新的数据对象引用,然后将它们作为道具传递给NormalChildComponent。(请注意,无论我们是使用function关键字重新定义onclick还是将其定义为箭头函数,这都无关紧要-无论哪种方式,它都是一个新的函数引用。)。

这也意味着,试图通过将主机组件(如<;div>;或<;button>;)包装在React.memo()中来优化它们的呈现是没有意义的。在这些基本组件下面没有子组件,所以渲染过程无论如何都会在那里停止。

但是,如果子组件试图通过检查道具是否已更改来优化渲染,则将新引用作为道具传递将使子组件渲染。如果新的道具引用实际上是新数据,这很好。但是,如果父组件只是向下传递回调函数怎么办?

const MemoizedChildComponent=React.memo(ChildComponent)函数ParentComponent(){const onclick=()=>;{console.log(";Button click";)}const data={a:1,b:2}return<;MemoizedChildComponent onclick={onclick}data={data}/>;}。

现在,每次ParentComponent呈现时,这些新引用将导致MemoizedChildComponent看到其道具值已更改为新引用,并且它将继续并重新呈现.。尽管每次onClick函数和数据对象应该基本上是相同的!

MemoizedChildComponent将始终重新呈现,即使我们想要跳过大部分时间的呈现。

它所做的比较新旧道具的工作是徒劳的。

同样,请注意,呈现<;MemoizedChild&><;OtherComponent/>;<;/MemoizedChild&>也会强制子项始终呈现,因为pros.Children始终是新引用。

类组件不必担心意外创建新的回调函数引用,因为它们可以拥有总是相同引用的实例方法。但是,它们可能需要为单独的子列表项生成唯一的回调,或者捕获匿名函数中的值并将其传递给子函数。这些将产生新的引用,因此在渲染时将创建新的对象作为子道具。Reaction没有任何内置功能来帮助优化这些案例。

对于函数组件,Reaction确实提供了两个钩子来帮助您重用相同的引用:useMemo用于任何类型的常规数据,如创建对象或执行复杂计算,以及useCallback专门用于创建回调函数。

如上所述,您不必将useMemo和Use Callback抛给您作为道具传递下来的每个单独的函数或对象-只有当它将对孩子的行为产生影响时才会抛出useMemo和Use Callback。(也就是说,useEffect的依赖项数组比较确实添加了另一个用例,其中子级可能希望接收一致的道具引用,这确实会使事情变得更加复杂。)。

另一个经常出现的问题是,为什么默认情况下没有反应将所有内容包装在React.memo()中?

丹·阿布拉莫夫曾多次指出,回忆录仍然会招致比较道具的费用,而且在很多情况下,回忆录检查永远不能阻止重新渲染,因为组件总是会收到新的道具。举个例子,看看这条来自丹的推特帖子:

为什么默认情况下Reaction不将memo()放在每个组件周围?是不是更快了?我们是否应该制定一个基准来检查?

为什么不在每个函数周围使用Lodash memoize()呢?那不是会让所有功能都更快吗?我们需要一个基准吗?为什么不行?

此外,虽然我没有关于它的特定链接,但由于人们在改变数据而不是一成不变地更新数据的情况下,尝试在默认情况下将其应用于所有组件可能会导致错误。

我已经在推特上与丹就此事进行了一些公开讨论。我个人认为,在广泛的基础上使用React.memo()很可能会在整体应用程序渲染性能上带来净收益。正如我去年在Twitter上的一篇长篇帖子中所说的那样:

作为一个整体,Reaction社区似乎过于痴迷于Perf&34;Perf&34;,然而,大部分讨论都是围绕着通过中等帖子和Twitter评论流传下来的过时的部落智慧,而不是基于具体的使用情况。

对于渲染的概念和性能的影响,肯定有集体的误解。是的,Reaction完全基于渲染-必须渲染才能做任何事情。不,大多数渲染并不太贵。

虚度光阴的复制者当然不是世界末日。也不是从根重新渲染整个应用程序,也就是说,浪费掉的没有DOM更新的重新渲染器是不需要烧毁的CPU周期,这也是事实。这对大多数应用程序来说是个问题吗?大概不会吧。有没有可以改进的地方?可能吧。

有没有默认的全部重新呈现方法不够用的应用程序?当然,这就是SCU、PureComponent和memo()存在的原因。

默认情况下,用户是否应该将所有内容都包装在memo()中?可能不会,如果仅仅是因为你应该考虑你的应用程序的性能需求。如果你这样做的话真的会疼吗?不,现实地说,我预计它确实有净收益(尽管丹关于浪费比较的观点)。

基准测试是否有缺陷,结果是否因场景和应用程序的不同而有很大差异?当然了。也就是说,如果人们可以开始指出这些讨论的硬数字,而不是玩电话游戏#34;我有一次看到一条评论.";这真的是很有帮助的。

我希望看到来自Reaction团队和更大的社区的一堆基准测试套件来衡量一系列场景,这样我们就可以一劳永逸地停止对大多数这些东西的争论。函数创建、渲染成本、优化.。请给我确凿的证据!

丹的标准答案是,应用程序的结构和更新模式千差万别,所以很难做出一个有代表性的基准。

还有一个关于什么时候不应该使用React.Memo的扩展问题讨论?在反应问题上。

(是的,这篇博文基本上是那条推文的延迟很久、扩展了很多的版本,尽管我实际上把所有这些都忘了,直到我刚刚在研究这篇帖子的时候偶然发现了这条推文。)。

使用Reaction DevTools Profiler查看每个提交中呈现的组件。找到意外呈现的组件,使用DevTools找出它们呈现的原因,然后修复问题(可能是通过将它们包装在React.memo()中,或者让父组件记下它传递的道具)。

另外,请记住,在dev构建中,Reaction的运行速度要慢得多。您可以在开发模式下分析您的应用程序,以了解哪些组件正在呈现以及为什么呈现,并将呈现组件所需的相对时间与其他组件进行比较(组件B在此提交中呈现所需的时间是组件A的3倍)。但是,千万不要使用Reaction开发构建-Only度量绝对呈现时间,而使用生产构建来度量绝对呈现时间!(否则丹·阿布拉莫夫将不得不因为你使用的数字不准确而对你大喊大叫)。请注意,如果您希望实际使用探查器从类似Prod的构建捕获计时数据,则需要使用React的特殊分析构建。

React的上下文API是一种机制,用于使单个用户提供的值可用于组件的子树,给定<;MyContext.Provider&>内的任何组件都可以从该上下文实例中读取值,而不必显式地将该值作为道具传递给每个中间的组件。(=>。

上下文不是状态管理工具。您必须自己管理传递到上下文中的值。这通常是通过将数据保持在反应组件状态,并基于该数据构造上下文值来实现的。

上下文提供程序接收单个值属性,如<;MyContext.Provider value={42}>;。子组件可以通过呈现上下文消费者组件并提供呈现道具来消费上下文,例如:

Reaction检查上下文提供程序在周围组件呈现该提供程序时是否已被赋予新值。如果提供程序的值是一个新的引用,那么Reaction知道该值已经更改,并且需要更新使用该上下文的组件。

请注意,将新对象传递给上下文提供程序将导致其更新:

函数GrandChild Component(){constvalue=useContext(MyContext);return<;div>;{value.A}<;/div>;}函数ChildComponent(){return<;GrandChild Component/>;}函数ParentComponent(){const[a,seta]=useState(0);const[b,setB]=useState(";text&#。/MyContext.Provider>;)}。

在本例中,每次呈现ParentComponent时,Reaction都会注意到MyContext.Provider已被赋予新值,并在MyContext继续向下循环时查找使用MyContext的组件。当上下文提供程序具有新值时,每个使用该上下文的嵌套组件都将被强制重新呈现。

请注意,从React角度来看,每个上下文提供程序只有一个值-无论是对象、数组还是基元,它只是一个上下文值。目前,使用上下文的组件无法跳过由新上下文值引起的更新,即使它只关心新值的一部分。

这意味着在默认情况下,呈现上下文提供程序的父组件的任何状态更新都将导致其所有后代重新呈现,而不管它们是否读取上下文值!

如果我们回顾上面的父/子/孙示例,我们可以看到GrandChild Component将重新呈现,但不是因为上下文更新-它将重新呈现,因为ChildComponent呈现了!在此示例中,没有任何东西试图优化以消除不必要的呈现,因此Reaction在任何时候ParentComponent呈现时都默认呈现ChildComponent和GrandChild Component。如果父级将n。

..