I’d like to start planning out the goals / vision of CanJS for the next year. These are ideas focused on just the “code”, and not ideas around evangelism/documentation/etc.
I haven’t thought it all through, but I see the following projects being important to keep pace with other OS tools, specifically React.
My high-level vision is to keep the benefits of CanJS (and some future CanJS plans) over react, while adopting the best of React.
I’d like to keep CanJS’s
- very strong observables, which are necessary to solving a lot of structural problems with react applications (way too much code in top container)
- ability to interface with other technologies (works great with normal jQuery widgets).
- the performance benefits of can-derive with live-binding
With react, I’d like to adopt:
- JSX style templating
Here’s what I think this means:
JSX style templating with live-binding
can-derive and live-binding can have algorithmically faster DOM updates than a react-style DIFF. A react style render and diff requires, at minimum a O(2n)
cost. One loop of data to render the VDOM, another loop to do the DIFF. can-derive
can do a filter in O( log(n)*log(n) ) = O( log^2 n)
time. This time can become significant at 64 items (128 vs 36 operations)
and quickly outweigh the usually dominant DOM operations.
Also, in big applications, if some shared bit of state is changed, the DIFF approach requires a re-render and diff of the entire page and all sub-components. Observables allow us to limit the scope of changes.
While diffing is great tech, I think we can provide functional programming and allow faster updates. Plus there are a lot of subtle things that observables make better, like list stores, that flux-like (observable-less) architectures make much more difficult.
What React really gets right, and what we abandoned from 1.1 to 2.0, is the use of “plain” JavaScript in its templates. This was done for two reasons:
-
The use of
plain
JS in templates was painful in EJS. This was largely due to the need to useattr
:<%= todo.attr("name") %>
-
It was difficult to make data-bindings work.
#1 will be addressed in the next section. For #2, it will be possible using much of the stache core technology (can/view/parser
, can/view/target
, can/view/live
) to make a new templating language similar to JSX, but uses live binding for the performance reasons listed above. For example, we can transpile:
function(viewModel) {
<child-component value={viewModel.propertyName}/>
}
into:
function(viewModel) {
return can.view.target([{
tag: "child-component",
attrs: {
value: can.compute(function(){ return viewModel.propertyName })
}
}]);
}
(note: the template binding syntax is not decided here … I’m using react-like syntax for an example)
Another example:
function(viewModel) {
<ul>
{
viewModel.items.map((item) => {
return <li><child-component value={item.value}></li>
})
}
</ul>
}
Becomes:
function(viewModel) {
return can.view.target([{
tag: "ul",
children: [
function(){
return viewModel.items.map((item) => {
return can.view.target([{
tag: "li",
children: [
{
tag: "child-component",
attrs: {value: can.compute(function(){ return item.value })}
}
]
}])
})
}
]
}]);
}
Note: if you are wondering why we don’t just use react, please see my comments on performance. If you are still not convinced, I can walk through a long architecture discussion on the importance of observables.
_Note: I’m also unhappy that we would transition to ANOTHER template language, however, we would make sure this works well with stache. Also, building HTML is a HUGE part of web-apps, so it’s not entirely unsuprising that this is where CanJS would consistantly see the most amount of change.
Priority: #4.
.attr
-less observables
Allow defining observables that work similar to can.Map
and can.List
, but can be used with the normal JS DOT (.) operator like:
var person = observe({});
person.first = "Justin";
person.last = "Meyer";
var fullName = can.compute(function(){
return person.first+" "+person.last;
});
QUnit.stop();
fullName.bind("change", function(ev, newVal, oldVal){
QUnit.start();
QUnit.equal(newVal, "Vyacheslav Egorov");
QUnit.equal(oldVal, "Justin Meyer");
});
// causes change event above
can.batch.start();
person.first = "Vyacheslav";
person.last = "Egorov";
can.batch.stop();
This project has a demo that works in browsers that support Proxies (Firefox / MS Edge). We can make all modern browsers work if we make people pre-define all their expected properties:
var Person = function(){}
can.define( Person.prototype, {
first: "*",
last: "string"
});
Progress on this can be found at can-define.
can-derive and lodash methods
can-derive is what enables algorthimically faster DOM updates than a diff. It uses a Red-Black tree version of can.List. But its use is pretty simple assuming .attr-less
observables:
var list = can.observe([1, 2, 3, 4, 5]);
var filtered = list.dFilter(function(item){
return item % 2
});
We should get all the common lodash methods working. Furthermore, the can-define
plugin should be made to work with this nicely:
var TodosListViewModel = function(){}
can.define( TodosListViewModel.prototype, {
items: {
value: function(resolve){
return Todo.getList().then(resolve);
}
},
completedItems: {
get: function(){
return this.items ? this.items.dFilter(function(todos){
return todos.complete;
}) : []
}
}
});
tlvm = new TodosListViewModel();
// the template:
function(viewModel){
<ul> {
viewModel.completedItems.map((item) => {
return <li><child-component value={item.value}></li>
})
}</ul>
}
Automatic Batching
Batching should be automatic. A batch should start as the last operation of the end of a requestAnimationFrame and end as the first operation of a requestOperationFrame. We should still allow can.batch.stop(true)
to flush events so synchronous testing can be done more easily.
“class” inheritable
Things like components should be inheritable with ES6.
class TodoList extends Component {
viewModel: TodosListViewModel
}
If this is not possible (because of the lack of extension hooks), we should might make those things simple functions:
TodoList = component(
viewModel: TodosListViewModel
})
Structural Changes
I think everything in CanJS should be accessible as it’s own npm module, available to Browserify/CJS, RequireJS/AMD, ES Modules and the steal()
syntax for legacy support. Each plugin will make itself available as a global so if people are still concating scripts, it’s still possible.
There will still be a can
module that will include the core modules on a namespace object.
I think we drop support for everything other than Zepto / jQuery and standalone.
A plan for getting there
Here’s a “shooting from the hip” roadmap.
- CanJS 3.0 - Basically CanJS 2.3, but with structural changes, stache/define plugin by default, Automatic Batching
- CanJS 3.1 - can-derive and lodash methods
- CanJS 3.2 -
.attr
-less observables - CanJS 3.3 - JSX style templating with live-binding