Reagent文档:管理状态
尽管可以通过使用 reagent.dom/render 重新挂载整个组件树来更新 reagent 组件, Reagent 提供了一个基于 reagent.core/atom 的复态管理机制, 允许组件跟踪应用程序状态, 并且只在需要时更新.
Reagent 还提供了r/cursor, 这些游标类似于r/atoms, 但可以从一个或多个其他r/atoms来构建, 以限制或扩展组件使用的r/atoms. 最后, Reagent 提供了一组称为r/reaction的原语和一组构建更定制化状态管理的实用函数.
原子(r/atom)
Reagent 提供了一个可以通过 reagent/atom 创建并像普通 Clojure atom一样使用的实现, 这些原子通常被称为 r/atoms, 以区别于atom. Reagent 跟踪组件渲染函数期间对 ratoms 的引用(deref).
(ns example
(:require [reagent.core :as r]))
(def click-count (r/atom 0))
(defn counting-component []
[:div
"The atom " [:code "click-count"] " has value: "
@click-count ". "
[:input {:type "button" :value "Click me!"
:on-click #(swap! click-count inc)}]])
变更一个 ratom
可以使用标准的 reset! 和 swap! 函数来操作.
(reset! state-atom {:counter 0})
(swap! state-atom assoc :counter 15)
引用一个 ratom
可以使用 deref 或简写 @ 来访问原子.
(:counter (deref state-atom))
(:counter @state-atom)
引用 ratom 的影响
在其渲染函数期间对 ratom 的引用将导致该组件在 ratom 的任何部分更新时重新渲染. (请参阅下面关于游标的部分, 以获得更细致的更新控制. ) 在渲染函数运行后, 在回调或事件处理程序中对 ratom 的引用不会使组件对 ratom 的任何更改作出反应(尽管当然在事件处理程序中对 ratom 进行的任何更改将使任何监视组件重新渲染).
rswap!
rswap! 的工作方式类似于标准的 swap!, 只是它:
- 总是返回
nil - 允许在同一个原子上递归应用
rswap!
这使得 rswap! 特别适用于事件处理.
这是一个使用 rswap! 进行事件处理的示例:
(defn event-handler [state [event-name id value]]
(case event-name
:set-name (assoc-in state [:people id :name] value)
:add-person (let [new-key (->> state :people keys (apply max) inc)]
(assoc-in state [:people new-key] {:name ""}))
state))
(defn emit [e]
;; (js/console.log "Handling event" (str e))
(r/rswap! app-state event-handler e))
(defn name-edit [id]
(let [p @(r/track person id)]
[:div
[:input {:value (:name p)
:on-change #(emit [:set-name id (.. % -target -value)])}]]))
(defn edit-fields []
(let [ids @(r/track person-keys)]
[:div
[name-list]
(for [i ids]
^{:key i} [name-edit i])
[:input {:type 'button
:value "Add person"
:on-click #(emit [:add-person])}]]))
所有事件都通过 emit 函数传递, 包括对 rswap! 的简单应用和一些可选的日志记录. 这是应用状态实际变化的唯一地方--其余部分都是纯函数.
实际的事件处理是在 event-handler 中完成的, 它以状态和事件作为参数, 并返回新的状态(事件在这里用向量表示, 事件名称在第一位).
所有 UI 组件要做的就是返回一些标记, 并通过 emit 函数设置事件的路由.
这种架构基本上将应用程序划分为两个逻辑功能:
- 第一个以状态和事件作为输入, 并返回下一个状态.
- 另一个以状态为输入, 并返回 UI 定义.
这个简单的应用程序可能也可以使用常见的 swap!, 而不是 rswap!, 但在 React 的事件处理程序中使用 swap! 可能会触发由于意外返回值而产生的警告, 并且如果由 emit 调用的事件处理程序本身发出新事件(这将导致事件丢失和很多混乱), 可能会引起严重的头痛.
有关类似方法的更结构化版本, 请参见优秀的 re-frame 框架.
游标(r/cursor)
任何引用r/atom的组件都将在atom新时更新(哪怕一点点的更新). 如果将所有状态存储在单个r/atom中(并不罕见), 将导致每当状态更新时每个组件都更新.
性能方面, 这可能是可以接受的, 这取决于有多少元素以及状态更新有多频繁, 因为 React 本身不会操纵 DOM, 除非组件实际更改.
Reagent 提供了游标(r/cursor), 这些游标的行为像原子, 但像指针一样指向更大原子的某一部分(或多个原子的多个部分).
游标是使用 reagent/cursor 创建的, 它需要一个 ratom 和一个键路径(类似 get-in):
;; 首先创建一个 ratom
(def state (reagent/atom {:foo {:bar "BAR"}
:baz "BAZ"
:quux "QUUX"}))
;; 现在创建一个游标
(def bar-cursor (reagent/cursor state [:foo :bar]))
(defn quux-component []
(js/console.log "quux-component is rendering")
[:div (:quux @state)])
(defn bar-component []
(js/console.log "bar-component is rendering")
[:div @bar-cursor])
(defn mount-root []
(rdom/render [:div [quux-component] [bar-component]]
(.getElementById js/document "app"))
(js/setTimeout (fn [] (swap! state assoc :baz "NEW BAZ")) 1000)
(js/setTimeout (fn [] (swap! state assoc-in [:foo :bar] "NEW BAR")) 2000))
bar-component 和 quux-component 都会在各自的游标/原子更新时更新, 但由于 bar-component 的游标仅限于应用状态的相关部分, 它仅在 [:foo :bar] 更新时重新渲染, 而 quux-component 则在应用状态每次更改时更新, 即使 :quux 从未更改.
更通用的游标
游标机制比上述描述的要通用得多. 可以传递一个函数, 该函数对一个或多个原子执行任意转换.
Reagent 还提供了 reagent/wrap 机制, 它也派生出一个新的原子, 但提供了更通用的功能. 在哪里游标总是更新其派生的原子, reagent/wrap 取一个原子和一个回调, 每当派生的原子更新时就会调用该回调.
用 (r/wrap first-name swap! n assoc :first-name) 替换 (r/cursor n [:first-name]) 本质上给出了相同的结果.
反应(r/reaction)
反应类似于使用函数调用的游标.
当反应产生一个新的结果(由 = 确定)时, 它们会导致其他依赖的反应和组件更新.
make-reaction 函数及其宏 reaction 用于创建反应, 这是一种属于多种协议的类型, 如 IWatchable, IAtom, IReactiveAtom, IDeref, IReset, ISwap, IRunnable 等.
使其类似于原子: 即它可以被监视, 取消引用, 重置, 交换, 并且此外, 还跟踪其取消引用, 表现出反应性等.
反应是赋予 r/atom, r/cursor 和 r/wrap 力量的原因.
make-reaction 接受一个参数 f, 和一个可选的map, 指定对 f 的处理:
auto-run(布尔值)指定 f 是否在变化时运行on-set和on-dispose在反应从 DOM 设置和取消设置时运行derefed未明确
反应在以下情况非常有用:
- 需要一种方式, 组件仅基于部分 ratom 状态更新时. (
reagent/cursor也可以用于此场景). - 想要组合两个 ratoms 并产生一个结果.
- 希望组件使用 ratom 的某些转换值.
一个示例:
(def app-state (reagent/atom {:state-var-1 {:var-a 2
:var-b 3}
:state-var-2 {:var-a 7
:var-b 9}}))
(def app-var2a-reaction (reagent.ratom/make-reaction
#(get-in @app-state [:state-var-2 :var-a])))
(defn component-using-make-reaction []
[:div
[:div "component-using-make-reaction"]
[:div "state-var-2 - var-a : " @app-var2a-reaction]])
下面的示例使用 reagent.ratom/reaction 宏, 它提供了与使用普通 make-reaction 相比的语法糖:
(let [username (reagent/atom "")
password (reagent/atom "")
fields-populated? (reagent.ratom/reaction (every? not-empty [@username @password]))]
[:div "Is username and password populated ?" @fields-populated?])
反应是异步执行的, 因此如果您依赖于反应的副作用, 请确保调用 flush.
跟踪函数(r/track)
reagent.core/track 接受一个函数和该函数的可选参数, 并给出一个可引用的(即"原子般的")值, 包含该函数返回的任何内容.
如果跟踪的函数依赖于 Reagent 原子, 每当该原子变化时就会再次调用该函数--就像 Reagent 组件函数一样. 如果组件中使用了 track 返回的值, 当函数返回的值变化时, 组件会重新渲染.
换句话说, @(r/track foo x) 给出与 (foo x) 相同的结果--但在第一种情况下, 只有当它依赖的原子(们)变化时, foo 才会被再次调用.
这是一个示例:
(ns example.core
(:require [reagent.core :as r]))
(defonce app-state (r/atom {:people
{1 {:name "John Smith"}
2 {:name "Maggie Johnson"}}}))
(defn people []
(:people @app-state))
(defn person-keys []
(-> @(r/track people)
keys
sort))
(defn person [id]
(-> @(r/track people)
(get id)))
(defn name-comp [id]
(let [p @(r/track person id)]
[:li
(:name p)]))
(defn name-list []
(let [ids @(r/track person-keys)]
[:ul
(for [i ids]
^{:key i} [name-comp i])]))
在这里, name-list 组件仅在 :people 映射的键变化时重新渲染. 每个 name-comp 只在需要时再次渲染等.
使用 track 可以通过三种方式提高性能:
- 它可以用作昂贵函数的缓存, 如果该函数依赖于 Reagent 原子(或其他 tracks, 游标等), 则会自动更新.
- 它还可以用来限制组件被重新渲染的次数. 只有当函数的结果改变时, track 的使用者才会更新. 换句话说, 您可以将 track 用作一种通用的只读游标.
- 使用相同参数的每个 track 只会导致函数执行一次. 例如, 上面示例中的两个
@(r/track people)只会导致一次对 people 函数的调用(初始时以及当状态原子改变时).
注意事项
与反应(reactions)相比, reagent.ratom/reaction 和 track 是相似的. 主要区别在于 track 使用命名函数和变量, 而不依赖于闭包, 并且不需要手动管理它们的创建(因为 tracks 是自动缓存和重用的).
注意: track 的第一个参数应该是一个命名函数, 即不是匿名函数. 同时, 要注意懒惰数据序列: 除非用 doall 包裹, 否则不要在 for 宏中使用引用(即" @" )(就像在 Reagent 组件中一样).
track! 函数
track! 的工作方式与 track 相同, 不同之处在于传递的函数会立即被调用, 并且只要其内部使用的任何原子发生变化, 就会继续被调用.
例如, 给定以下函数:
(defn log-app-state []
(prn @app-state))
可以使用 (defonce logger (r/track! log-app-state)) 来监控 app-state 的变化. log-app-state 将继续运行, 直到使用 (r/dispose! logger) 停止它.