July 21, 2019
By: Kevin

Reagent(React)深入学习第二部分

欢迎来到第二部分!

第一部分我们看到了如何生成一个reagent组件, 组件如何挂载属性, 响应事件, 在数据的驱动下的状态变化. 接下来的更有挑战:

  1. 如何处理内部状态
  2. 如何访问DOM, 包装three.js的一个场景来呈现3D

Reagent component的几种形式

我们已经知道了怎么通过deref一个全局的Atom来保证组件刷新, 驱动组件变化. 但是世界很复杂, 如下两种情况都不是现有模式可以处理的:

  1. 如果我们希望一个组件持有一个自有的状态, 也就是一个局部的Atom, 而不是全局的Atom, 这样我们可以创建状态分离的, 高度可服用的组件.
  2. 如果我们希望一个组件调用某个函数, 这个函数是作用于DOM节上的, 所以只能等这个组件render完成之后才可以调用. 有很多的JavaScript直接操 做实际的DOM元素. 我们需要有机制能够保证在组件创建相应的DOM元素后才调用.

Reagent 的组件充分考虑了以上情形, 提供了三种形式:

  1. 组件可以是一个返回hiccup vector的函数, 我们称之为Form1
  2. 组件可以是一个返回函数的函数, 我们称之为Form2
  3. 组件可以是一个返回Class的函数, 我们称之为Form3

第一种形式我们已经司空见惯. 第二种形式可以借助函数闭包的特性, 来持有私有状态

首先让我们引入reagent

(require '[reagent.core :as r]
         '[reagent.dom :as rdom])
(defn greetings []
  (fn []
    [:h3 "Hello world"]))

这个例子中, 组件greetings返回一个函数, 这个函数返回hiccup.

动手: 尝试下修改代码, 为上面这个组件加上你想要的状态, 熟悉返回函数的函数的方式.

Form2可以通过let绑定局部Atom的方式为后面返回的函数保存状态

(defn timer-component []
  (let [seconds-elapsed (r/atom 0)]
    (fn []
      (js/setTimeout #(swap! seconds-elapsed inc) 1000)
      [:div
       "Seconds Elapsed: " @seconds-elapsed])))

上面的例子持有了一个局部Atom, 实现了有状态的计数器

再看一个例子, 两个组件分别持有自己的状态

(defn a-better-mouse-trap [mouse]
  (let [mice (r/atom 1)]
    (fn render-mouse-trap [mouse]
      (into
       [:div
        [:button
         {:on-click
          (fn [e]
            (swap! mice (fn [m] (inc (mod m 4)))))}
         "Catch!"]]
       (repeat @mice mouse)))))

[:div
 [a-better-mouse-trap
  [:img
   {:src "https://www.domyownpestcontrol.com/images/content/mouse.jpg"
    :style {:width "120px" :border "1px solid"}}]]
 [a-better-mouse-trap
  [:img
   {:src "https://avatars1.githubusercontent.com/u/9254615?v=3&s=150"
   :style {:width "120px" :border "1px solid"}}]]]

理解了From2, 也就理解了closure = 变量捕获

Reagent看到我们使用了很多的Form2, 稍微有点繁琐, 所以提供了with-let


(defn lambda [rotation x y]
  [:g {:transform (str "translate(" x "," y ")"
                       "rotate(" rotation ") ")}
   [:circle {:r 50, :fill "green"}]
   [:circle {:r 25, :fill "blue"}]
   [:path {:stroke-width 12
           :stroke "white"
           :fill "none"
           :d "M -45,-35 C 25,-35 -25,35 45,35 M 0,0 -45,45"}]])

(defn spinnable []
  (r/with-let [rotation (r/atom 0)]
              [:svg
               {:width 120 :height 120
                :on-mouse-move
                (fn [e]
                  (swap! rotation + 30))}
               [lambda @rotation 60 60]]))


(defn several-spinnables []
  [:div
   [:h3 "Move your mouse over me"]
   [a-better-mouse-trap [spinnable]]])

那么Form3是什么样子的呢?

(defn announcement []
  (r/create-class
   {:reagent-render
    (fn []
      [:h3 "我就是传说中的Form3."])}))

reagent-render 看起来很眼熟, 因为它就是我们的render函数, 和其它form中的并无区别, 只不过这个组件整体上返回一个React class, 可以挂载多个生命周期函数, 生命周期函数我们接下来就会用到.

React Class 生命周期函数包括:

  1. Mounting (Occurs once when the component is created)
    • constructor
    • componentWillMount
    • render
    • componentDidMount
  2. Updating (Occurs many times as the component reacts to change)
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  3. Unmounting (Occurs once when the component will be removed from the DOM)
    • componentWillUnmount

更详细信息可以从 生命周期 官方文档获取

在Reagent里我们可以简化下, 只使用两个

  • componentDidMount 让我们在组件成功加载到DOM后调用
  • componentWillUnmount 允许我们在组件卸载前做些清理工作

Form3确实比较繁琐, Reagent的with-let可以避免使用willUnmount的情景, 它提供一个finally语句, 可以在此进行资源清理.

(defn mouse-position []
  (r/with-let [pointer (r/atom nil)
               handler (fn [e]
                   (swap! pointer assoc
                       :x (.-pageX e)
                           :y (.-pageY e)))
                _ (js/document.addEventListener "mousemove" handler)]
                [:div "Pointer moved to: " (str @pointer)]
                  (finally
                    (js/document.removeEventListener "mousemove" handler))))

那Form3还有用武之地吗? 只剩下一个场景了, 即直接访问DOM的情况. 16.3版本的React引入了ref 机制, 似乎Form3在最后的领域也有竞争对手了. Anyway, 让我们看看Form3的表演:

(defn create-renderer [element]
  (doto (js/THREE.WebGLRenderer. #js {:canvas element :antialias true})
    (.setPixelRatio js/window.devicePixelRatio)))

(defn three-canvas [attributes camera scene tick]
  (let [requested-animation (atom nil)]
    (r/create-class
     {:display-name "three-canvas"
      :reagent-render
      (fn three-canvas-render []
        [:canvas attributes])
      :component-did-mount
      (fn three-canvas-did-mount [this]
        (let [e (rdom/dom-node this)
              r (create-renderer e)]
          ((fn animate []
             (tick)
             (.render r scene camera)
             (reset! requested-animation (js/window.requestAnimationFrame animate))))))
      :component-will-unmount
      (fn [this]
        (js/window.cancelAnimationFrame @requested-animation))})))

这个例子很完整的展示了Form3的整个流程

  • reagent-render 返回Hiccup
  • component-did-mount 调用的时机是组件已经mount到DOM, 开始动画
  • component-will-unmount 调用的时机是组件从DOM卸载, 停止动画

比较优雅的管理了生命周期, 这样

动画loop只有在mount以后启动, 此loop卸载之前就会停止

让我看看这个3D Scene

(defn create-scene []
  (doto (js/THREE.Scene.)
    (.add (js/THREE.AmbientLight. 0x888888))
    (.add (doto (js/THREE.DirectionalLight. 0xffff88 0.5)
            (-> (.-position) (.set -600 300 600))))
    (.add (js/THREE.AxisHelper. 50))))

(defn mesh [geometry color]
  (js/THREE.SceneUtils.createMultiMaterialObject.
   geometry
   #js [(js/THREE.MeshBasicMaterial. #js {:color color :wireframe true})
        (js/THREE.MeshLambertMaterial. #js {:color color})]))

(defn fly-around-z-axis [camera scene]
  (let [t (* (js/Date.now) 0.0002)]
    (doto camera
      (-> (.-position) (.set (* 100 (js/Math.cos t)) (* 100 (js/Math.sin t)) 100))
      (.lookAt (.-position scene)))))

(defn v3 [x y z]
  (js/THREE.Vector3. x y z))

(defn lambda-3d []
  (let [camera (js/THREE.PerspectiveCamera. 45 1 1 2000)
        curve (js/THREE.CubicBezierCurve3.
               (v3 -30 -30 10)
               (v3 0 -30 10)
               (v3 0 30 10)
               (v3 30 30 10))
        path-geometry (js/THREE.TubeGeometry. curve 20 4 8 false)
        scene (doto (create-scene)
                (.add
                 (doto (mesh (js/THREE.CylinderGeometry. 40 40 5 24) "green")
                   (-> (.-rotation) (.set (/ js/Math.PI 2) 0 0))))
                (.add
                 (doto (mesh (js/THREE.CylinderGeometry. 20 20 10 24) "blue")
                   (-> (.-rotation) (.set (/ js/Math.PI 2) 0 0))))
                (.add (mesh path-geometry "white")))
        tick (fn []
               (fly-around-z-axis camera scene))]
    [three-canvas {:width 150 :height 150} camera scene tick]))

这个3D组件可以和我们的普组件无缝衔接使用

[:div
 [a-better-mouse-trap [lambda-3d]]
 [a-better-mouse-trap [spinnable]]]

让我们再定义一个更加复杂的场景



(def pyramid-points
  [[-0.5 -0.5 0 "#63B132"] [-0.5 0.5 0 "#5881D8"] [0.5 0.5 0 "#90B4FE"] [0.5 -0.5 0 "#91DC47"] [0 0 1 "white"]])

(defn add-pyramid [scene x y z size color]
  (.add scene
        (doto
          (let [g (js/THREE.Geometry.)]
            (set! (.-vertices g)
                  (clj->js (for [[i j k] pyramid-points]
                             (v3 i j k))))
            (set! (.-faces g)
                  (clj->js (for [[i j k] [[0 1 2] [0 2 3] [1 0 4] [2 1 4] [3 2 4] [0 3 4]]]
                             (js/THREE.Face3. i j k))))
            (mesh g color))
          (-> (.-position) (.set x y z))
          (-> (.-scale) (.set size size size)))))

(defn add-pyramids [scene x y z size color]
  (if (< size 4)
    (add-pyramid scene x y z (* size 1.75) color)
    (doseq [[i j k color] pyramid-points]
      (add-pyramids scene
                    (+ x (* i size))
                    (+ y (* j size))
                    (+ z (* k size))
                    (/ size 2)
                    color))))

(defn gasket-3d []
  (let [camera (js/THREE.PerspectiveCamera. 45 1 1 2000)
        scene (doto (create-scene)
                (add-pyramids 0 0 0 32 "white"))
        tick (fn [] (fly-around-z-axis camera scene))]
    [three-canvas {:width 640 :height 640} camera scene tick]))
Tags: Reagent react clojurescript