反应组件和渲染之间的路由器差异

Posted

技术标签:

【中文标题】反应组件和渲染之间的路由器差异【英文标题】:react router difference between component and render 【发布时间】:2018-06-17 10:45:00 【问题描述】:

我真的不明白反应路由器中 Route 中的渲染和组件道具之间的区别,在文档中它说渲染不会创建新元素但组件会,我试图回到历史但我发现 componentWillMount 是当我在 Route 中使用渲染时调用,它们是什么意思“如果您为组件属性提供内联函数,您将在每次渲染时创建一个新组件。这会导致现有组件卸载和新组件安装而不是仅仅更新现有组件。”

【问题讨论】:

【参考方案1】:

The source code 告诉你区别:

if (component)
  return match ? React.createElement(component, props) : null

if (render)
  return match ? render(props) : null

当您使用 component 属性时,每次调用 Route#render 都会实例化组件。这意味着,对于传递给 Route 的 component 属性的组件,构造函数、componentWillMountcomponentDidMount 将在每次渲染路由时执行。

例如,如果你有

<Route path="/:locale/store" component=Store />

并且用户导航到 /en/store,然后去其他地方,然后导航回 /en/store,Store 组件将被挂载,然后卸载,然后再次挂载。类似于做

<Route path="/:locale/store">
  <Store />
</Route>

与此相比,如果您使用 render 属性,则组件会在每个 Route#render评估。还记得每个组件都是一个函数吗?该函数将按原样执行,没有任何生命周期方法。所以当你喜欢它时

<Route path="/:locale/store" render=Store />

你可以把它想象成

<Route path="/:locale/store">
  Store()
</Route>

它可以节省您的运行时间,因为没有运行生命周期方法,但它也有一个缺点,以防 Store 组件具有一些安装后的生命周期方法,例如 shouldComponentUpdate 也可以提高性能。


有a good post on Medium about this performance hack,请看一下。它写得很好,也适用于 React 16。

【讨论】:

在对该 Medium 帖子的回复中,与声明为类的有状态组件相比,有 a confirmation that the second option is even faster in react@16。 非常感谢,我只是很困惑,因为当我使用渲染时,我发现调用了 componentWillMount,如果我在某个时候更改状态并转到另一条路线,然后回到我的组件我发现初始状态不是更改的状态,文档不是说“更新现有组件而不是卸载它” Store() 如果这个组件已经有生命周期方法,如果我这样调用它,在没有任何生命周期方法的情况下如何执行 所以我可以这么说,我使用带有函数组件的渲染和带有类组件的组件 一个警告是,如果你使用 &lt;Route path="/:locale/store" render=Store /&gt; 并且你还在 Store 中使用了 react 钩子,那么它不会起作用,因为 react 钩子只能在函数组件中调用,而不是在常规函数中调用.【参考方案2】:

所以我也对这部分文档感到困惑,但我终于弄明白了。

理解这一点的关键是语句“为组件prop提供内联函数

我们都知道 Route 组件会在位置改变时重新渲染,react 会比较新旧虚拟 DOM 树,得到一些 diff 结果并应用到真实 DOM。

而且 react 会尽量复用 DOM 节点,除非新 ReactElement 的 typekey 属性发生变化。

所以

// 1.
const componentA = React.createElement(App, props)
const componentB = React.createElement(App, props)
console.log(componentA.type === componentB.type)             // true

// 2.
const componentA = React.createElement(() => <App />, props)
const componentB = React.createElement(() => <App />, props)
console.log(componentA.type === componentB.type)             // false

方式1创建的所有ReactElement都具有相同的类型(App组件),但如果它们都是通过方式2创建的,则它们的类型不同。

为什么?

因为在调用父组件(包含Route组件的组件)的render方法时,总会有一个新的匿名函数以方式2创建,所以new&old ReactElement的类型是两个不同的实例匿名函数

() => <App />

所以从 React 的角度来看,有不同类型的元素,应该用 unmount old > mount new 操作来处理,这意味着你对旧组件所做的每个状态或更改每次都会丢失父组件重新渲染。

但是为什么 render prop 会避免卸载和挂载行为呢?这也是一个匿名函数!?

这里我想参考@Rishat Muhametshin 贴出的代码,Route 组件的渲染方法的核心部分:

if (component)
  // We already know the differences:
  // React.createElement(component)
  // React.createElement(() => <component/>)
  return match ? React.createElement(component, props) : null

if (render)
  return match ? render(props) : null

render prop 是一个在调用时返回 ReactElement 的函数,该返回元素的类型是什么?

<Route render=() => <AppComponent />></Route>

它是 AppComponent,而不是匿名函数包装器!因为jsx编译后:

render = () => React.createElement(AppComponent)
render() = React.createElement(AppComponent)

React.createElement(render) =
  React.createElement(() => React.createElement(AppComponent))

React.createElement(render()) =
  React.createElement(React.createElement(AppComponent))

所以当你使用 render 而不是 component prop 时,render prop 函数返回的元素类型不会在每次渲染时改变,即使在每个 parentElement.render 上总是创建一个新的匿名函数实例()

在我看来,您可以通过为匿名函数命名来实现与 render prop 对组件 prop 所做的相同的行为:

// Put this line outside render method.
const CreateAppComponent = () => <AppComponent />

// Inside render method
render()
  return <Route component=CreateAppComponent/>

所以结论是,如果你直接使用component=AppComponent,组件和渲染道具之间没有性能差异,如果你想为AppComponent分配一些道具,使用 render=() =&gt; &lt;AppComponent ...props/&gt; 而不是 component=() =&gt; &lt;AppComponent ...props/&gt;

【讨论】:

完美!我想我终于明白了!谢谢!【参考方案3】:

大部分概念已经被其他答案解释了,让我整理一下:

首先,我们有source code:

if (component)
  return match ? React.createElement(component, props) : null

if (render)
  return match ? render(props) : null

案例#1:没有功能的组件

<Route path="/create" component=CreatePage />

React.createElement(CreatePage, props) 被调用是因为源代码中的React.createElement(component, props)。实例化将导致 重新安装

案例#2:无功能渲染

<Route path="/create" render=CreatePage />

React.createElement(CreatePage, props) 在传递给 render prop 之前被调用,然后由源代码中的render(props) 调用。无需实例化,无需重新安装。

案例#3:具有功能的组件

<Route path="/create" component= () => <CreatePage />  />

React.createElement(CreatePage, props) 被称为 两次。首先用于 jsx 解析(匿名函数),首先用于从匿名函数返回 CreatePage 的实例,其次用于从源代码返回。那么为什么不在组件属性中这样做呢。

oligofren指出的错误:

解析 JSX 不会调用它。它只是最终创建了函数表达式。你不想做 #3 的原因是你每次都创建一个新的匿名类型,导致重新挂载 dom。

案例#4:用函数渲染

<Route path="/create" render= () => <CreatePage />  />

每次路由到path=/create时都有一个实例化(jsx解析)。感觉像案例#1吗?

结论

根据这四种情况,如果我们想将prop传递给Component,我们需要使用case #4来防止重新挂载。

<Route path="/abc" render=()=><TestWidget num="2" someProp=100/>/>

这离主题有点远,所以我留下official discussion 以供进一步阅读。

【讨论】:

"React.createElement(CreatePage, props) 被调用两次。" 不,不会。解析 JSX 不会调用它。它只是最终创建了函数表达式。你不想做 #3 的原因是你每次都创建一个新的匿名类型,导致重新挂载 dom。 是的,你是对的,我当时写得不好,重要的是anonymous在调用时创建一次,源代码,总共两次。感谢您指出您的反对意见,提醒我错误。 但是您的回答仍然说它被调用了两次,但是当您将匿名函数分配给 render 道具时,它不会被调用。它被创建一次(当你分配它时)并且在render() method of Route 调用它之前不会被调用。并且只有 通过该调用 被调用 React.createElement,因为它隐含地是匿名函数的一部分(在 JSX 解析之后)。 对不起,我误会了你。我有一段时间没有使用 React,也许我应该参考你的评论:)【参考方案4】:

即使我们不向ComponentToRender 传递任何道具,我也发现使用render 代替component 有一些好处。 默认情况下,&lt;Route \&gt; 在使用 component 时将额外的 props( history, location, match ) 传递给 ComponentToRender。我们也可以通过 render 回调来访问这个道具,但我们也可以省略它。 为什么我们需要它? &lt;Route /&gt;'s 父级或任何导航的每次渲染(即使将路线更改为与以前相同)都会创建新的 match 对象。 所以当我们将它传递给我们的ComponentToRender 时,我们每次都会得到新的道具,这可能会导致一些性能问题,尤其是PureComponent

【讨论】:

以上是关于反应组件和渲染之间的路由器差异的主要内容,如果未能解决你的问题,请参考以下文章

反应路由器不渲染组件

嵌套反应路由器组件不会在页面重新加载时加载/渲染?

反应路由器不渲染组件然后他有参数

反应路由器本机渲染链接组件超过前一个

反应路由器 dom 重定向问题。更改 url,不渲染组件

当我在路由中使用渲染时反应返回 Outlet 组件