June 2, 2021
By: Kevin

Reagent文档:管理状态

  1. 原子(r/atom)
    1. 变更一个 ratom
    2. 引用一个 ratom
    3. 引用 ratom 的影响
    4. rswap!
  2. 游标(r/cursor)
    1. 更通用的游标
    2. 反应(r/reaction)
    3. 跟踪函数(r/track)
    4. 注意事项
    5. track! 函数

尽管可以通过使用 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-componentquux-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/cursorr/wrap 力量的原因.

make-reaction 接受一个参数 f, 和一个可选的map, 指定对 f 的处理:

  • auto-run(布尔值)指定 f 是否在变化时运行
  • on-seton-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 可以通过三种方式提高性能:

  1. 它可以用作昂贵函数的缓存, 如果该函数依赖于 Reagent 原子(或其他 tracks, 游标等), 则会自动更新.
  2. 它还可以用来限制组件被重新渲染的次数. 只有当函数的结果改变时, track 的使用者才会更新. 换句话说, 您可以将 track 用作一种通用的只读游标.
  3. 使用相同参数的每个 track 只会导致函数执行一次. 例如, 上面示例中的两个 @(r/track people) 只会导致一次对 people 函数的调用(初始时以及当状态原子改变时).

注意事项

与反应(reactions)相比, reagent.ratom/reactiontrack 是相似的. 主要区别在于 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) 停止它.

Tags: Reagent react