Tweaks mostly.
This commit is contained in:
parent
61e2964c6a
commit
a9c8d76e1e
58
README.md
58
README.md
|
@ -15,6 +15,7 @@ re-frame is still Alpha. But getting closer.
|
|||
Be sure you use v0.5.0 of reagent.
|
||||
|
||||
Todo:
|
||||
- provide a hook for logging and exception handling
|
||||
- implement pure event handlers. A macro will be needed.
|
||||
|
||||
<!--
|
||||
|
@ -27,11 +28,13 @@ alt="Leiningen logo" title="The man himself" align="right" />
|
|||
Either:
|
||||
|
||||
1. You want to develop an app in ClojureScript, and you are looking for a framework; or
|
||||
2. You believe that ReactJS decisively won the javascript framework wars in early 2015 and
|
||||
you are wondering how the combination of
|
||||
"reactive programming", "functional programming" and "immutable data" could
|
||||
**change everything**. Could this be a case of:
|
||||
"The future is already here - it's just not very evenly distributed".
|
||||
2. You believe that by early 2015 ReactJS had effectively won the javascript framework wars and
|
||||
you are curious about the bigger implications. Could it be that the combination of
|
||||
`reactive programming`, `functional programming` and `immutable data`
|
||||
**will completely change everything**? If so, what does that look like in a language
|
||||
that naturally embodies those paradigms?
|
||||
|
||||
> The future is already here - it's just not very evenly distributed.
|
||||
|
||||
|
||||
## re-frame
|
||||
|
@ -60,7 +63,7 @@ Features:
|
|||
5. The surprising thing about re-frame is how simple it is. Beautifully simple! Our reference
|
||||
implementation is little more than 100 lines of (clojurescript) code. Learn it in an afternoon.
|
||||
6. But it scales up so nicely to more complex apps. Frameworks are just pesky overhead at small
|
||||
scale - its how they help you to tame complexity that defines them.
|
||||
scale - its how they help you to tame the complexity of bigger apps that measure them.
|
||||
7. Re-frame is impressively buzzword compliant: it has FRP-nature,
|
||||
unidirectional data flow, pristinely pure functions, conveyor belts, statechart-friendliness (FSM)
|
||||
and claims an immaculate hammock conception.
|
||||
|
@ -71,7 +74,8 @@ __Warning__: this is a long document. That was the summary.
|
|||
|
||||
## What Problem Does It Solve?
|
||||
|
||||
First we decided to build apps with ClojureScript, then we choose [Reagent], then we had a problem.
|
||||
First we decided to build our SPA apps with ClojureScript, then we
|
||||
choose [Reagent], then we had a problem.
|
||||
|
||||
For all its considerable brilliance, Reagent (+ ReactJS)
|
||||
delivers only the 'V' part of a traditional MVC framework.
|
||||
|
@ -97,10 +101,10 @@ and there's clearly a 'V' bit and there's a layer which is
|
|||
Yes, that's true. But to quote McCoy: "It's MVC, Jim,
|
||||
but not as we know it".
|
||||
|
||||
In re-frame none of the M, V, or C bits are objects, they
|
||||
In re-frame, none of the M, V, or C bits are objects, they
|
||||
are pure functions (or pure data), and
|
||||
and they are all wired together via reactive data flows. It is just such a sufficiently different
|
||||
version of (traditional, smalltalk) MVC that calling it MVC would likely just be confusing. I'd
|
||||
and they are all wired together via reactive data flows. It is sufficiently different in nature
|
||||
from (traditional, smalltalk) MVC that calling it MVC would likely just be confusing. I'd
|
||||
love an alternative.
|
||||
|
||||
Perhaps it is a RACES framework - Reactive-Atom Component Event
|
||||
|
@ -117,12 +121,9 @@ insider's joke, conference T-Shirt.
|
|||
Not much about re-frame is original or clever. You'll find
|
||||
no ingenious use of functional zippers, transducers or core.async.
|
||||
|
||||
Re-frame does use Reagent's features in a novel way.
|
||||
And we did actively reject
|
||||
the current ClojureScript fashion of using Cursors (which turned
|
||||
out to be a terrific decision).
|
||||
|
||||
But apart from that, for the most part, re-frame is a mashup of
|
||||
Re-frame does use Reagent's features in a slightly novel way.
|
||||
And we did actively reject the current ClojureScript fashion of using Cursors.
|
||||
But, for the most part, re-frame is just a mashup of
|
||||
emerging ideas.
|
||||
|
||||
(For the record,
|
||||
|
@ -147,7 +148,6 @@ At small scale, any framework or architecture seems like pesky overhead. The
|
|||
explanatory examples in here are necessarily small scale, so you'll need to
|
||||
squint a little to see the benefits that accrue at larger scale.
|
||||
|
||||
|
||||
## Guiding Philosophy
|
||||
|
||||
__First__, above all we believe in the one true [Dan Holmsand], creator of Reagent, and
|
||||
|
@ -163,9 +163,10 @@ But you'll only really "get"
|
|||
Reagent when you view it as an FRP-ish library. To put that another way, we think
|
||||
that Reagent, at its best, is closer in nature to [Hoplon] or [Elm] than it is OM.
|
||||
|
||||
__Finally__, we believe in one-way data flow. No two way data binding. We don't like read/write `cursors` which
|
||||
promote the two way flow of data. As programs get bigger, we've found that their use seems to
|
||||
encourage control logic into all the wrong places.
|
||||
__Finally__, we believe in one-way data flow. No two way data binding. We don't
|
||||
like read/write `cursors` which
|
||||
promote the two way flow of data. As programs get bigger, we've found that their
|
||||
use seems to encourage control logic into all the wrong places.
|
||||
|
||||
## FRP Clarifications
|
||||
|
||||
|
@ -276,7 +277,7 @@ this is also very easy. Saving past states is trivial, and you will automaticall
|
|||
good sharing guarantees to keep the size of the snapshots down.
|
||||
|
||||
|
||||
To this list of benefits, I would briefly add two: the ability to genuinely model control via FSMs
|
||||
To this list, I would briefly add two: the ability to genuinely model control via FSMs
|
||||
and the ability to do time travel debugging, even in a production setting. More on both soon.
|
||||
|
||||
[Hoplon] takes the same approach via what they called `stem cells`, which is a root source of data.
|
||||
|
@ -305,13 +306,14 @@ Richard Dawkins
|
|||
|
||||
### How Flow Happens In Reagent
|
||||
|
||||
To implement FRP, Reagent provides a `ratom` and a `reaction`. re-frame uses both of these
|
||||
building blocks, so let's now make sure we understand them before going further.
|
||||
To implement FRP, Reagent provides a `ratom` and a `reaction`.
|
||||
re-frame uses both of these
|
||||
building blocks, so let's now make sure we understand them.
|
||||
|
||||
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
|
||||
|
||||
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame
|
||||
perspective, we'll tweak that paradigm ever so slightly and **view a `ratom` as being a value that
|
||||
perspective, we'll tweak that paradigm ever so slightly and **view a `ratom` as having a value that
|
||||
changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a
|
||||
`ratom` as a Signal. [Pause and read this](http://elm-lang.org/learn/What-is-FRP.elm).
|
||||
|
||||
|
@ -755,8 +757,8 @@ that it chains `reactions`:
|
|||
```
|
||||
|
||||
The original version had only one `reaction` which would be re-run completely each time `app-db` changed.
|
||||
The new version, has chained reactions.
|
||||
The 1st and 2nd reactions just extract from `db`. They will fire each time `db-app` changes.
|
||||
This new version, has chained reactions.
|
||||
The 1st and 2nd reactions just extract from `db`. They will run each time `db-app` changes.
|
||||
But they are cheap. The 3rd one does the expensive
|
||||
computation using the result from the first two.
|
||||
|
||||
|
@ -910,6 +912,7 @@ They `dispatch` pure data and nothing more.
|
|||
Collectively, event handlers provide the control logic in a re-frame application.
|
||||
|
||||
An event handler is a pure function of two parameters:
|
||||
|
||||
1. current value in `app-db`. Note: that's the map **in** `app-db`, not the atom itself.
|
||||
2 an event (represented as a vector)
|
||||
|
||||
|
@ -946,6 +949,7 @@ app-db --> components --> Hiccup --> Reagent --> VDOM --> React -->
|
|||
```
|
||||
|
||||
The `router` will:
|
||||
|
||||
1. inspect the 1st element of the arriving vector
|
||||
2. look in its registry for the handler which is registered for this kind of event
|
||||
3. call that handler with two parameters: (1) the current value in `app-db` and (2) the event vector
|
||||
|
@ -1102,7 +1106,7 @@ modify `app-db` themselves. That is always done in a handler.
|
|||
|
||||
## The CPU Hog Problem
|
||||
|
||||
Sometime a handler has a lot of CPU intensive work to do, and it takes a while to get through.
|
||||
Sometimes a handler has a lot of CPU intensive work to do, and getting through it will take a while.
|
||||
|
||||
When a handler hogs the CPU, nothing else can happen. Browsers only give us one thread of
|
||||
execution and that CPU-hogging handler owns it, and it isn't giving it up. The UI will be
|
||||
|
|
|
@ -11,8 +11,7 @@
|
|||
:profiles {:debug {:debug true}
|
||||
:dev {:dependencies [[spellhouse/clairvoyant "0.0-48-gf5e59d3"]]
|
||||
|
||||
:plugins [[lein-cljsbuild "1.0.4"]
|
||||
#_[com.cemerick/clojurescript.test "0.3.3" ]]}}
|
||||
:plugins [[lein-cljsbuild "1.0.4"]]}}
|
||||
|
||||
|
||||
:clean-targets [:target-path
|
||||
|
@ -28,8 +27,7 @@
|
|||
;; - lein install
|
||||
;; :jar-exclusions [#"(?:^|\/)re-frame-demo\/"]
|
||||
|
||||
:cljsbuild {:builds [;; currently bogus, there is no demo
|
||||
{:id "demo"
|
||||
:cljsbuild {:builds [{:id "demo" ;; currently bogus, there is no demo or tests
|
||||
:source-paths ["src"]
|
||||
:compiler {:output-to "run/compiled/demo.js"
|
||||
:source-map "run/compiled/demo.js.map"
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
(:require [reagent.core :as r]))
|
||||
|
||||
;; The application state
|
||||
;; Should not be accessed directly by application code (neither handlers or subscriptions)
|
||||
;; Should never be accessed directly by application code
|
||||
;; Access is mediated via handlers and subscriptions
|
||||
(def app-db (r/atom {}))
|
||||
|
||||
|
||||
|
|
|
@ -3,16 +3,11 @@
|
|||
(:require-macros [cljs.core.async.macros :refer [go-loop go]])
|
||||
(:require [reagent.core :refer [flush]]
|
||||
[re-frame.db :refer [app-db]]
|
||||
[re-frame.utils :refer [first-in-vector]]
|
||||
[re-frame.utils :refer [first-in-vector warn]]
|
||||
[cljs.core.async :refer [chan put! <! timeout]]))
|
||||
|
||||
|
||||
(defn warn
|
||||
[& args]
|
||||
(.warn js/console (apply str args)))
|
||||
|
||||
|
||||
;; -- register of handlers ------------------------------------------------------------------------
|
||||
;; -- register of event handlers ------------------------------------------------------------------------
|
||||
|
||||
(def ^:private id->fn (atom {}))
|
||||
|
||||
|
@ -20,20 +15,18 @@
|
|||
"register a handler for an event"
|
||||
[event-id handler-fn]
|
||||
(when (contains? @id->fn event-id)
|
||||
(warn "Overwriting an event-handler" event-id)) ;; allow it, but warn.
|
||||
(warn "re-frame: overwriting an event-handler" event-id)) ;; allow it, but warn.
|
||||
(swap! id->fn assoc event-id handler-fn))
|
||||
|
||||
|
||||
;; -- The Event Conveyor Belt --------------------------------------------------------------------
|
||||
;; A channel which moves events from dispatch to handlers.
|
||||
;;
|
||||
;; 1. "dispatch" puts events onto this chan, and
|
||||
;; 2. "router" reads from the chan, and calls associated handlers
|
||||
;; This enables async handling of events -- which is a good thing.
|
||||
(def ^:private event-chan (chan))
|
||||
;; Moves events from "dispatch" to the router loop.
|
||||
;; Key architecutal purpose is to cause aysnc handling of events.
|
||||
(def ^:private event-chan (chan)) ;; TODO: how big should we make the buffer?
|
||||
|
||||
|
||||
;; -- router --------------------------------------------------------------------------------------
|
||||
;; -- lookup and call -----------------------------------------------------------------------------
|
||||
|
||||
(defn- handle
|
||||
"Look up the handler for the given event, then call it, passing in 2 parameters."
|
||||
|
@ -41,20 +34,22 @@
|
|||
(let [event-id (first-in-vector event-v)
|
||||
handler-fn (get @id->fn event-id)]
|
||||
(if (nil? handler-fn)
|
||||
(warn "No event handler registered for event: " event-id )
|
||||
(warn "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") ;; TODO: make exception
|
||||
(handler-fn app-db event-v))))
|
||||
|
||||
|
||||
;; In a loop, read events from the dispatch channel, and then call the
|
||||
;; right handler.
|
||||
;; -- router loop ---------------------------------------------------------------------------------
|
||||
;;
|
||||
;; In a loop, read events from the dispatch channel, and route them
|
||||
;; to the right handler.
|
||||
;;
|
||||
;; Because handlers occupy the CPU, before each event is handled, hand
|
||||
;; back control to the GUI render process, via a (<! (timeout 0)) call.
|
||||
;; back control to the browser, via a (<! (timeout 0)) call.
|
||||
;;
|
||||
;; In odd cases, we need to pause for an entire annimationFrame, to ensure that
|
||||
;; the DOM is fully flushed, before calling a handler known to hog the CPU
|
||||
;; for an extended period. In that case the event should have metadata
|
||||
;; Example:
|
||||
;; the DOM is fully flushed, before thencalling a handler known to hog the CPU
|
||||
;; for an extended period. In such a case, the event should have metadata
|
||||
;; Example usage:
|
||||
;; (dispatch ^:flush-dom [:event-id other params])
|
||||
;;
|
||||
;; router loop
|
||||
|
@ -67,7 +62,7 @@
|
|||
(recur)))
|
||||
|
||||
|
||||
;; -- helper --------------------------------------------------------------------------------------
|
||||
;; -- dispatch ------------------------------------------------------------------------------------
|
||||
|
||||
(defn dispatch
|
||||
"reagent components use this function to send events.
|
||||
|
@ -75,13 +70,13 @@
|
|||
(dispatch [:delete-item 42])"
|
||||
[event-v]
|
||||
(if (nil? event-v)
|
||||
(warn "dispatch is ignoring a nil event.") ;; nil would close the channel
|
||||
(warn "re-frame: \"dispatch\" is ignoring a nil event.") ;; nil would close the channel
|
||||
(put! event-chan event-v)))
|
||||
|
||||
|
||||
;; TODO: remove sync handling. I don't like it, even for testing.
|
||||
(defn dispatch-sync
|
||||
"sync version of above that actually does the dispatch"
|
||||
"Invoke the event handler sycronously, avoiding the async-inducing use of core.async/chan"
|
||||
[event-v]
|
||||
(handle event-v))
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
(ns re-frame.subs
|
||||
(:require
|
||||
[re-frame.db :refer [app-db]]
|
||||
[re-frame.utils :refer [first-in-vector]]))
|
||||
[re-frame.utils :refer [first-in-vector warn]]))
|
||||
|
||||
|
||||
;; mappings from handler-id to handler-fn
|
||||
;; maps from handler-id to handler-fn
|
||||
(def ^:private key->fn (atom {}))
|
||||
|
||||
|
||||
(defn register
|
||||
"register a function for a handler id"
|
||||
"register a hander function for an id"
|
||||
[key-v handler-fn]
|
||||
(if (contains? @key->fn key-v)
|
||||
(println "Warning: overwritting a subscription-handler: " key-v)) ;; TODO: more generic logging
|
||||
(warn "re-frame: overwriting subscription-handler for: " key-v)) ;; allow it, but warn.
|
||||
(swap! key->fn assoc key-v handler-fn))
|
||||
|
||||
|
||||
|
@ -21,5 +21,6 @@
|
|||
[v]
|
||||
(let [key-v (first-in-vector v)
|
||||
handler-fn (get @key->fn key-v)]
|
||||
(assert (not (nil? handler-fn)) (str "No subscription handler registered for key: " key-v))
|
||||
(handler-fn app-db v)))
|
||||
(if (nil? handler-fn)
|
||||
(warn "re-frame: no subscription handler registered for: \"" key-v "\". Returning nil.")
|
||||
(handler-fn app-db v))))
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
(ns re-frame.utils)
|
||||
|
||||
|
||||
(defn warn
|
||||
[& args]
|
||||
(.warn js/console (apply str args)))
|
||||
|
||||
|
||||
(defn first-in-vector
|
||||
[v]
|
||||
(assert (vector? v) (str "Expected a vector event, but got: " v))
|
||||
(first v))
|
||||
(if (vector? v)
|
||||
(first v)
|
||||
(warn "re-frame: expected a vector event, but got: " v)))
|
||||
|
|
Loading…
Reference in New Issue