As discussed in my cljs architecture post post, frontend architecture decomposes as two orthogonal concerns:
- DOM/view manipulation
- state propagation
Angular takes the DOM seriously by associating view models (“scopes”) with DOM subtrees automatically. This is by far the best approach I’ve seen, especially when it comes to collaborating with designers—they can manipulate plain HTML markup as they see fit, without having to worry about tweaking tags, classes, or IDs that the application might depend on. However, since core.async offers nothing for DOM manipulation, I won’t discuss it further.
As for state propagation, Angular uses a dirty checking loop that invokes all watches on all scopes until they reach a fixed point. (See the “dirty checking” section of the cljs architecture post for more details.)
Pros of Angular’s approach:
- All view state is totally explicit. For a DOM element to depend on some application data, that data must be exposed on the scope associated with that DOM element. Holding state within directives or holding references to the DOM within controllers is EXTREMELY discouraged.
- Semantically the dirty-checking model is simple and enables decoupling of concerns.
For example, we built an application with a date-range selector consisting of two date pickers, associated with
startDate
andendDate
objects on the scope. We added a single watch that depended on both dates and ensured that endDate was always after startDate. This semantic existed as a single watch, separate from other domain logic that relied on startDate/endDate.
Cons of Angular’s approach:
- All view state totally explicit.
This can be considered a pro or a con, depending on how you feel about view-model properties like
isLoading
. - The dirty checking loop can be expensive (especially on mobile), since even a total no-op requires that all of the watch sentinel functions are executed at least once. When developing the Weathertron, we found that on an iPod touch 4th gen each watch fn cost on average about 1 ms (and we had 200+ watches, mostly from data-binding within the DOM).
- The dirty checking loop and its watcher functions implies (controlled) mutable state.
Core.async is pretty fresh when it comes to UI, and there aren’t a ton of established patterns. One of the most interesting (pointed out by David Nolen) is that core.async enables local event loops, in contrast to the single global event loop of vanilla JS. People are writing
(go (loop [my-thing (<! a-channel)
with ... other ... state ...]
...
(recur (<! a-channel) with other state)))
throughout their applications; one go block loop per concern. (See Bruce Hauman’s core.async dots game and David Nolen’s autocompleter.)
However, the community seems to agree that go blocks should be stateless and I haven’t seen any patterns emerging for sharing explicit state between go blocks.
Furthermore, even if it’s conceptually nice to have separate local event loops, when it comes to browser performance, DOM manipulation (i.e., painting and/or layout) dominates typical JavaScript processing time. Thus, it’s better to batch DOM updates from (potentially unrelated) application processes rather than to let each local process manipulate the DOM directly. (In fact, this is exactly what David does in his go block benchmark.)
Open questions
Core.async is definitely exciting, and I’ve been very happy using it in Clojure on the JVM to abstract ZeroMQ sockets and websockets. When it comes to DOM-based UIs, though, I have a few open questions.
What is the appropriate level of granularity for channels and go blocks?
For each data binding (e.g., <span>My name is {{name}}</span>
) you could have a local event loop blocking on a channel containing the latest value of name
.
This seems extreme to me; requiring a lot of multiplexed channel plumbing for seemingly little benefit over, say, a map within an atom.
(I’d love for someone to take the conceptually elegant idea of “everything is a go block” and implement it elegantly in practice to prove me wrong.)
Another phrasing of this question: how do view models / presentation models fit together with core.async concurrency?
How can local event loops be coordinated to batch DOM manipulations? If a single button click changes the application domain model that forces 5 conceptually independent views to update, can their updates be coordinated without over-coupling the views?
Do view widgets need objects/processes in memory when they’re not computing? Both Angular.js directives and go block local event loops take up memory in their “resting” state (Angular scope objects, go block stacks, and the native JavaScript event handlers with references to said objects and channels). What would a view widget look like if it only had a footprint in memory when it was being interacted with by the user or updating based on a change in application domain models?