试剂:在另一个组件中定义组件是否会导致性能问题?

时间:2016-11-18 05:51:27

标签: clojure clojurescript reagent

在Reagent中,我们假设我正在定义一个辅助函数,该函数返回父组件的渲染函数中的子组件

这是否会在每次渲染函数运行时生成新的子组件?

这是一个最小的例子来说明:

(defn ChildComponent [text]
  [:p text])

(defn ParentComponent [names-vector]
  (let [renderChild (fn [i] 
                     [ChildComponent (get names-vector i)])]
    [:div
     [renderChild 1]
     [renderChild 3]
     [renderChild 5]]))

我在renderChild中定义了一个let函数作为辅助函数,以避免每次使用(get names-vector i)时重复ChildComponent

我希望这几乎完全等同于:

(defn ChildComponent [text]
  [:p text])

(defn ParentComponent [names-vector]
  [:div
   [ChildComponent (get names-vector 1)]
   [ChildComponent (get names-vector 3)]
   [ChildComponent (get names-vector 5)]])

names-vector中的更改可能会触发重新呈现ChildComponents,但不会触发销毁和创建。

Reagent是否将第一个示例扩展到第二个示例?或者由于重复的组件破坏/创建,第一个示例是否存在潜在的重大性能问题?

3 个答案:

答案 0 :(得分:1)

我一直在使用Reagent大约一年了,我就是这样做的 事情。我会说这是建议做的事情。试剂也会 隐式使用参数作为指标来知道是否重新渲染 组件与否,因此它非常适合于知道何时重新渲染。现在, 我的用例可能比其他一些用户小,但我一直都是 这种设置在性能方面非常高兴。尽我所能 可以说,这是鼓励的设计模式。

要注意的一件事是处理类似的清单 元素(表格行,项目列表等),您可能想要附加元数据 唯一标识每个兄弟姐妹。 React将使用这个引擎盖进行优化 渲染。

因此,在您的情况下,您可能需要更多类似的内容:

[:div
 ^{:key 1} [renderChild 1]
 ^{:key 2} [renderChild 3]
 ^{:key 5} [renderChild 5]]))

<强>更新

所以,我猜错了:这种技术的问题是创建了一个新函数 每次重新渲染父级时,Reagent将会看到并被强制使用 调用新函数来获取潜在的新子项 - 因为“组件” 似乎已经改变了。我也没有以同样的方式使用这些功能 原始海报做了。相反,当我需要重复元素时,我选择了 形式:

(into []
      (for [i [1 3 5]]
        [ChildComponent (get names-vector i)]))

或者你可以改为:

(let [renderChild (fn [i]
                   [ChildComponent (get names-vector i)])]
 (into []
       (for [i [1 3 5]]
         (renderChild i))))

第一种形式简单地避免了额外的功能定义。第二种形式 导致renderChild在返回数据之前被评估,因此Reagent永远不会 看到临时功能 - 因此侧面需要使用Reagent 评估它,发现孩子们没有改变。

但是在查看我们的应用程序的代码时,我们选择了第一个表单 在所有情况下的第二个,或简单地打破功能,并给它一个 名字 - 它有意义。

此外,这个例子很差:

[:div
 ^{:key 1} [renderChild 1]
 ^{:key 2} [renderChild 3]
 ^{:key 5} [renderChild 5]]))

添加密钥仅在列表元素的数量会发生变化时才有意义。 AFAICT,React使用该信息来帮助优化差异计算和 允许它更容易地计算哪些成员需要被丢弃以及哪些成员被丢弃 添加。如果它是静态的,那么无所谓。一个更好的例子是:

(into [:div]
      (for [val some-coll]
       ^{:key (compute-key val)} [:p (get some-other-coll val)]))

some-coll的内容可能发生变化。

答案 1 :(得分:0)

我已经进一步探讨了这个问题,遗憾的是,如果组件定义发生在另一个组件的渲染函数的let绑定中,则Reagent似乎不会缓存组件。

用于实验的示例代码:

(defonce app-state (r/atom 0))

(defn Clicker []
  [:h2 {:on-click #(swap! app-state inc)} "Click me"])

(defn Displayer [n]
  (let [a (r/atom (js/console.log "Component generated!"))]
    (fn [n]
      [:p "Hello, yes I ignore my input"])))

(defn Intermediate1 [n]
  [Displayer n])

(defn Intermediate2 [n]
  (let [Subcomponent (fn [x] [Displayer x])]
    [Subcomponent n]))

(defn my-app []
  [:div
   [Clicker]
   [Intermediate2 @app-state]])

(r/render
  [my-app]
  (js/document.getElementById "app"))

使用Intermediate1组件,消息“Component generated!”应用程序启动时,控制台日志上会显示一次。

使用Intermediate2,控制台日志显示“已生成组件!”每次点击Clicker组件时。

^{:key 1}内添加Intermediate2不会改变结果:

   (defn Intermediate2 [n]
      (let [Subcomponent (fn [x] [Displayer x])]
        ^{:key 1} [Subcomponent n]))

这表明每次向Displayer发送新值时,Intermediate2组件都会被销毁并生成,这将触发重新呈现。 (或者更确切地说,渲染新创建的组件)

因此,虽然在实践中成本可能并不高(特别是对于生成少量简单组件),但似乎let内部渲染函数中的绑定组件会导致不必要的组件破坏/创建,可能会否定使用React样式虚拟DOM的效率优势。

答案 2 :(得分:0)

请在提问之前阅读此链接。

https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#the-three-ways

数据结构块或数据结构的函数返回块将被视为Form-1。所有这些都将作为单一数据结构进行扩展。此范围内的任何更改都将触发重新呈现此单一数据结构。

  

这是否会在每次渲染函数运行时生成新的子组件?

是。由于renderChild性质,我们会重新创建Form-1

  

我在let中定义了一个renderChild函数作为辅助函数,以避免每次使用ChildComponent时都重复(get names-vector i)。

如果您希望ChildComponent中的任何一个只在其自己的(get names-vector i)值更改时重新呈现,而不是在父项或兄弟项中更改时重新呈现。您应该使用Form-2Form-3。 (名字 - 矢量必须是使试剂起作用的一种方法!

(defn ChildComponent [names-vector i]
  (fn [names-vector i]
    [:p (get @names-vector i)]))

(defn ParentComponent [names-vector]
  [:div
   [ChildComponent names-vector 1]
   [ChildComponent names-vector 3]
   [ChildComponent names-vector 5]])