Reagent(React)深入学习第二部分
欢迎来到第二部分!
第一部分我们看到了如何生成一个reagent组件, 组件如何挂载属性, 响应事件, 在数据的驱动下的状态变化. 接下来的更有挑战:
- 如何处理内部状态
- 如何访问DOM, 包装three.js的一个场景来呈现3D
Reagent component的几种形式
我们已经知道了怎么通过deref一个全局的Atom来保证组件刷新, 驱动组件变化. 但是世界很复杂, 如下两种情况都不是现有模式可以处理的:
- 如果我们希望一个组件持有一个自有的状态, 也就是一个局部的Atom, 而不是全局的Atom, 这样我们可以创建状态分离的, 高度可服用的组件.
- 如果我们希望一个组件调用某个函数, 这个函数是作用于DOM节上的, 所以只能等这个组件render完成之后才可以调用. 有很多的JavaScript直接操 做实际的DOM元素. 我们需要有机制能够保证在组件创建相应的DOM元素后才调用.
Reagent 的组件充分考虑了以上情形, 提供了三种形式:
- 组件可以是一个返回hiccup vector的函数, 我们称之为Form1
- 组件可以是一个返回函数的函数, 我们称之为Form2
- 组件可以是一个返回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 生命周期函数包括:
- Mounting (Occurs once when the component is created)
- constructor
- componentWillMount
- render
- componentDidMount
- Updating (Occurs many times as the component reacts to change)
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
- 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返回Hiccupcomponent-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]))