为什么多方法在Reagent/Re-frame中不能像函数一样工作?

20
在我构建的一个小应用程序中,使用Reagent和Re-frame,我正在使用多方法根据应用程序状态中的值来分派显示哪个页面:
(defmulti pages :name)

(defn main-panel []
  (let [current-route (re-frame/subscribe [:current-route])]
    (fn []
      ;...
      (pages @current-route))))

然后我有以下方法:

(defmethod layout/pages :register [_] [register-page])

其中register-page函数将生成实际视图:

(defn register-page []
  (let [registration-form (re-frame/subscribe [:registration-form])]
    (fn []
      [:div
       [:h1 "Register"]
       ;...
       ])))

我尝试更改我的应用程序,使方法直接生成页面,例如:

(defmethod layout/pages :register [_]
  (let [registration-form (re-frame/subscribe [:registration-form])]
    (fn []
      [:div
       [:h1 "Register"]
       ;...
       ])))

这导致没有页面被呈现。在我的主面板中,我将调用pages的方式更改为方括号,以便Reagent可以对其进行可见性处理:

(defn main-panel []
  (let [current-route (re-frame/subscribe [:current-route])]
    (fn []
      ;...
      [pages @current-route])))

这导致第一个访问的页面可以工作,但之后点击链接(会导致当前路由更改)没有效果。

在包含init函数的文件中,所有定义单个方法的命名空间都是必需的,而我可以选择任何一个单独的页面并将其显示出来,这证明了代码正在加载(然后切换到另一页不起作用):

https://github.com/carouselapps/ninjatools/blob/master/src/cljs/ninjatools/core.cljs#L8-L12

为了调试究竟发生了什么,我定义了两个路由::about:about2,一个作为函数,一个作为方法:

(defn about-page []
  (fn []
    [:div "This is the About Page."]))

(defmethod layout/pages :about [_]
  [about-page])

(defmethod layout/pages :about2 [_]
  (fn []
    [:div "This is the About 2 Page."]))

并使布局打印调用pages函数的结果(当然必须使用显式调用而不是方括号)。包装后的函数,也就是起作用的函数,返回:

[#object[ninjatools$pages$about_page "function ninjatools$pages$about_page(){
return (function (){
return new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"div","div",1057191632),"This is the About Page."], null);
});
}"]]

当该方法返回时:

#object[Function "function (){
return new cljs.core.PersistentVector(null, 2, 5, cljs.core.PersistentVector.EMPTY_NODE, [new cljs.core.Keyword(null,"div","div",1057191632),"This is the About 2 Page."], null);
}"]

如果我把方法改成:

(defmethod layout/pages :about2 [_]
  [(fn []
     [:div "This is the About 2 Page."])])

也就是说,将函数返回到一个向量中,然后它开始工作。如果我对被包装的函数进行相反的更改,它将以与该方法相同的方式失败:

(defn about-page []
  (fn []
    [:div "This is the About Page."]))

(defmethod layout/pages :about [_]
  about-page)

Reagent的语法是[function],这有点合理,但它本应自动调用函数。

我还开始向浏览器输出@current-route

[:main.container
 [alerts/view]
 [pages @current-route]
 [:div (pr-str @current-route)]]

我已验证@current-route正在被正确修改并更新输出,只是没有更新[pages @current-route]

我的应用程序的完整源代码可以在此处找到:https://github.com/carouselapps/ninjatools/tree/multi-methods

更新:根据Michał Marczyk的答案更正了方法的参数数量。


你是否需要在某个根命名空间中使用defmethod?因为如果你不显式地要求这些命名空间,你的方法就不会被添加到多方法中。 - skrat
@skrat 是的。我会编辑问题来说明这一点。 - pupeno
为了调试这个问题,我会做两件事:1. 添加一个默认方法;2. 打印出多态方法返回的内容(可能是nil)。 - PeakCode
@PeakCode 我确实有一个默认方法:https://github.com/carouselapps/ninjatools/blob/multi-methods/src/cljs/ninjatools/layout.cljs#L14 如果正在呈现该方法,我将得到一个空白的主要内容。相反,页面只是不刷新。 - pupeno
@PeakCode 当方法在方括号中时,我不是自己调用它,所以打印它的输出很困难。 - pupeno
4个回答

15

所以,像这样的组件:[pages @some-ratom]
pages@some-ratom发生变化时,它将重新渲染。

从reagent的角度来看,pages自上次以来并没有改变,它仍然是之前的多方法。但是@some-ratom可能会改变,因此可能会触发重新渲染。

但是当重新渲染发生时,它将使用pages的缓存版本进行。毕竟,对于reagent来说,pages似乎没有改变。它仍然是之前的多方法。

当然,pages的缓存版本将是呈现的第一个版本-多方法的第一个版本,而不是我们期望看到的新版本。

Reagent进行此缓存是因为它必须处理Form-2函数。它必须保留返回的渲染函数。

底线是:由于缓存的存在,多方法在使用时可能不太好用,除非你找到一种完全摧毁组件并重新开始的方法,这正是当前得票最高的方法所做的:
^ {:key @current-route} [pages @current-route]
当然,彻底摧毁组件并重新开始可能会有其自身不受欢迎的影响(取决于该组件中保存的本地状态)。

相关背景:
https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#appendix-a---lifting-the-lid-slightly
https://github.com/Day8/re-frame/wiki/When-do-components-update%3F


有没有一种优雅的替代多方法的方式,而不会带来不良影响? - zengod

5

我不是很清楚细节,但显然,当我渲染这样的页面时:

[:main.container
 [alerts/view]
 [pages @current-route]]

Reagent没有注意到 pages 依赖于@ current-route 的值。使用 Chrome React插件帮助我找到了问题所在。我尝试使用ratom而不是订阅,这似乎工作正常。谢天谢地,告诉Reagent / React元素的键很容易

[:main.container
 [alerts/view]
 ^{:key @current-route} [pages @current-route]]

那很好。


1
reagent/React 会在每次键值(key)改变时销毁并重新创建页面组件。所以这种方法是可行的,但要注意它的工作方式。 - Mike Thompson
1
只是为了明确,这个语句:“Reagent未能注意到页面依赖于@current-route的值”是不正确的。请查看我的答案,了解发生了什么。 - Mike Thompson

2
我看到的第一个问题是你的方法没有参数:
(defmethod layout/pages :register [] [register-page])
                                  ^ arglist

这里有一个空的参数列表,但是你可能会用一个或两个参数来调用这个多方法(因为它的分派函数是一个关键字,关键字可以用一个或两个参数调用)。

如果你想用一个参数调用这个多方法,并且只是在:register方法体内忽略它,请将上面的内容更改为:

(defmethod layout/pages :register [_] [register-page])
                                   ^ argument to be ignored

另外,我想你可能会想要像之前一样调用pages(也就是说,撤销你在问题中提到的方括号的更改)。


这可能会修复应用程序,也可能不会 - 可能还有其他问题 - 但它应该让你开始了。(如果您传入任何参数,多方法绝对无法使用那些空的参数列表。)


关于参数列表的观点很好。我认为ClojureScript只是不检查函数和方法的arity。我已经做了这个更改,但应用程序仍然以相同的方式运行:https://github.com/carouselapps/ninjatools/commit/971de441b46e407857625d5c8f0b90fd78a54dd0 - pupeno

1
如果您使用一个包装器函数pages-component,它是一个常规函数,可以被reagent缓存,那会怎么样呢?它将如下所示:
(defn pages-component [state]
  (layout/pages @state))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接