Brace yourselves: a tsunami of text approaches. I've been squirreling away on this branch for a while now without sharing the progress, which, although necessary β due to a) the magnitude of the changes and b) uncertainty over whether they'd pan out β isn't how I like to work. This PR (which isn't ready for merge) is intended to bring the changes into the open and explain how 0.8 will most likely differ from 0.7.
Those of you who follow Ractive's development will be aware of #1740, an epic pull request by @martypdx. Marty had a Eureka moment a while back relating to how data changes propagate through Ractive instances, and #1740 applied that insight to the existing codebase. Unfortunately, as I contributed to #1740, I came to realise that the technical debt in the codebase (incurred when Ractive was my solo project, I hasten to add) made such large-scale changes extremely difficult, and I began to wish we could rewrite it as though Marty had designed it in the first place.
This is that rewrite. It's based on #1740, and aims to achieve the following:
- Replace the 0.7 (and earlier) viewmodel concept with a lighter tree-like structure (as per #1740)
- Apply the same thinking to the way DOM updates are applied. More on this below
- Make the codebase lighter and more readable
The first of these is all about performance. While Ractive comfortably bests most of its competitors in typical scenarios when it comes to DOM update performance, templates with hundreds of components are sluggish on initial render, which is often when performance is most critical. This is largely due to some convoluted caching logic that becomes unnecessary when you model things the right way.
The second is also performance related, but is also a big win for simplicity. As with the data side of things, thinking about DOM updates in terms of trees makes a lot of the indirection in the current codebase unnecessary. Tricky issues like #1986 should be much easier to resolve.
The third bullet point is perhaps the most important. Time and again, people tell us that they'd love to contribute, but that the codebase is too overwhelming to get a handle on. That's a great shame, and left unchecked will slow Ractive's development down. This PR won't fix that in one go, but I'm confident that it's a major step in the right direction.
Executive summary of the changes
Fewer modules
The current codebase is characterised by lots of tiny modules. In many cases that's unnecessary, because those modules are only used in one place (e.g. they define a method on an internal class). In this PR, you're more likely to see one file per class than one file per method. This is partly an aesthetic choice (I think it's easier to read), but largely motivated by the use of ES6 classes, which have a more streamlined syntax that encourages this form. (The class
keyword is much maligned, but is actually very well suited to Ractive's ontology, at least internally.)
Note: our build process will need to be updated to accommodate classes β at present, Babel's classCallCheck
helper is included many times when it only needs to be there once.
viewmodel -> model, virtualdom -> view
Strictly speaking it doesn't make sense to say that these folders have been renamed, since their contents are all new. But the responsibilities of the things formerly found in src/viewmodel
now reside in src/model
, and likewise with src/virtualdom
and src/view
.
Partly this is because even though Ractive isn't an MVC library in the traditional sense, those terms are well-understood and the division of responsibilities is easy to grok. But mostly it's because I like to be able to use bash autocomplete after typing the first couple of letters of a directory name, and 'v, i, TAB, nope, e, whoops, BACKSPACE, r, TAB' was driving me mad.
Trees
As alluded to, 0.8 is all about trees. Each value in your data
object is represented by a Model
. If you have this...
var ractive = new Ractive({
data: {
foo: {
bar: 42
},
baz: 'childless'
}
});
...then the root node has two children with the keys foo
and baz
. foo
is a branch (with a child, bar
), baz
is a leaf. Each node is aware of its immediate parent-child relationships, and of things that depend on its value. Models are created 'lazily' (i.e. as and when they become necessary for Ractive to track).
An instance's 'root model' (the equivalent of ractive.viewmodel
in 0.7 β will most like become ractive.model
, I suppose) is just another node, with a few minor differences; ditto computed values.
As soon as a model changes value (internally, model.set(newValue)
), each of its dependencies are notified of the change (via their handleChange
methods). There's no longer any need to distinguish between different types of dependant (e.g. computation, observer, virtual DOM node). When a virtual DOM node (hereafter, 'view') is notified in this way, it marks itself as 'dirty' and 'bubbles' the notification. Parent nodes (fragments containing the view, and elements/sections etc containing those fragments) are also marked as dirty and continue propagating the notification (unless they were already dirty).
The end result of this is that a top-level fragment is marked as dirty. Once all the data changes have been propagated, and observers etc have been called, we can 'clean' it. Because we're thinking in trees, we can disregard already-clean branches (the bits that don't need to change), and ignore any children of things that no longer need to exist.
(Almost) no breaking changes
I'm hopeful that the only breaking changes will be desirable ones (e.g. #1776). If there are any breaking changes as a result of this rewrite, they'll be signposted in the documentation, but so far it looks good. (About 5% of the tests are currently failing, but most of them look like they'll be remedied reasonably straightforwardly.)
If you read this far, congratulations on your stamina. As ever, feedback and questions are warmly welcomed. Thanks!