redraw strategy

This commit is contained in:
Leo Horie 2014-08-11 23:15:53 -04:00
parent 44119f43f7
commit 4281773df9
7 changed files with 182 additions and 26 deletions

View file

@ -6,7 +6,7 @@ This page aims to provide a comparison between Mithril and some of the most wide
### Code Size ### Code Size
One of the most obvious differences between Mithril and most frameworks is in file size: Mithril is around 4kb gzipped and has no dependencies on other libraries. One of the most obvious differences between Mithril and most frameworks is in file size: Mithril is around 5kb gzipped and has no dependencies on other libraries.
Note that while a small gzipped size can look appealing, that number is often used to "hide the weight" of the uncompressed code: remember that the decompressed Javascript still needs to be parsed and evaluated on every page load, and this cost (which can be in the dozens of milliseconds range for some frameworks in some browsers) cannot be cached. Note that while a small gzipped size can look appealing, that number is often used to "hide the weight" of the uncompressed code: remember that the decompressed Javascript still needs to be parsed and evaluated on every page load, and this cost (which can be in the dozens of milliseconds range for some frameworks in some browsers) cannot be cached.

View file

@ -4,7 +4,7 @@
Mithril is a client-side Javascript MVC framework, i.e. it's a tool to make application code divided into a data layer (called **M**odel), a UI layer (called **V**iew), and a glue layer (called **C**ontroller) Mithril is a client-side Javascript MVC framework, i.e. it's a tool to make application code divided into a data layer (called **M**odel), a UI layer (called **V**iew), and a glue layer (called **C**ontroller)
Mithril is around 4kb gzipped thanks to its [small, focused, API](mithril.md). It provides a templating engine with a virtual DOM diff implementation for performant rendering, utilities for high-level modelling via functional composition, as well as support for routing and componentization. Mithril is around 5kb gzipped thanks to its [small, focused, API](mithril.md). It provides a templating engine with a virtual DOM diff implementation for performant rendering, utilities for high-level modelling via functional composition, as well as support for routing and componentization.
The goal of the framework is to make application code discoverable, readable and maintainable, and hopefully help you become an even better developer. The goal of the framework is to make application code discoverable, readable and maintainable, and hopefully help you become an even better developer.

View file

@ -48,7 +48,7 @@
<div class="feature col(4,4,12)"> <div class="feature col(4,4,12)">
<h2>Light-weight</h2> <h2>Light-weight</h2>
<ul> <ul>
<li>Only 4kb gzipped, no dependencies</li> <li>Only 5kb gzipped, no dependencies</li>
<li>Small API, small learning curve</li> <li>Small API, small learning curve</li>
</ul> </ul>
</div> </div>

View file

@ -18,10 +18,65 @@ If you are developing an asynchronous model-level service and finding that Mithr
--- ---
### Changing redraw strategy
If you need to change how Mithril performs redraws, you can change the value of the `m.redraw.strategy` getter-setter to either `"all"`, `"diff"` or `"none"`. By default, this value is set to `"all"` when running controller constructors, and it's set to `"diff"` for all subsequent redraws.
```javascript
var module1 = {}
module1.controller = function() {
//this module will attempt to diff its template when routing, as opposed to re-creating the view from scratch.
//this allows config contexts to live across route changes, if its element does not need to be recreated by the diff
m.redraw.strategy("diff")
}
module1.view = function() {
return m("h1", {config: module1.config}, "test")
}
module1.config = function(el, isInit, ctx) {
if (!isInit) ctx.data = "foo"
}
```
---
### Preventing redraws on events
Similarly, it's possible to skip redrawing altogether by calling `m.redraw.strategy("none")`
```javascript
m("input", {onkeydown: function(e) {
if (e.keyCode == 13) ctrl.save() //do things and re-render only if the `enter` key was pressed
else m.redraw.strategy("none") //otherwise, ignore
}})
```
---
### Signature ### Signature
[How to read signatures](how-to-read-signatures.md) [How to read signatures](how-to-read-signatures.md)
```clike ```clike
void redraw() void redraw() { GetterSetter strategy }
```
where:
GetterSetter :: String getterSetter([String value])
```
- <a name="strategy"></a>
### m.redraw.strategy
**GetterSetter strategy**
The `m.redraw.strategy` getter-setter indicates how the next module redraw will occur. It can be one of three values:
- `"all"` - recreates the DOM tree from scratch
- `"diff"` - updates only DOM elements if needed
- `"none"` - leaves the DOM tree intact
This value can be programmatically changed in controllers and event handlers to modify the next redrawing strategy. It is modified internally by Mithril to the value `"all"` before running controller constructors, and to the value `"diff"` after all redraws.
Calling this function without arguments returns the currently assigned redraw strategy.

View file

@ -91,7 +91,7 @@ app.view = function(ctrl) {
[How to read signatures](how-to-read-signatures.md) [How to read signatures](how-to-read-signatures.md)
```clike ```clike
void render(DOMElement rootElement, Children children) void render(DOMElement rootElement, Children children [, Boolean forceRecreation])
where: where:
Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Array<Children children> Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Array<Children children>
@ -112,3 +112,6 @@ where:
If it's a list, its contents will recursively be rendered as appropriate and appended as children of the `root` element. If it's a list, its contents will recursively be rendered as appropriate and appended as children of the `root` element.
- **Boolean forceRecreation**
If set to true, rendering a new virtual tree will completely overwrite an existing one without attempting to diff against it

View file

@ -309,6 +309,7 @@ Mithril = m = new function app(window, undefined) {
function autoredraw(callback, object, group) { function autoredraw(callback, object, group) {
return function(e) { return function(e) {
e = e || event e = e || event
m.redraw.strategy("diff")
m.startComputation() m.startComputation()
try {return callback.call(object, e)} try {return callback.call(object, e)}
finally { finally {
@ -339,12 +340,13 @@ Mithril = m = new function app(window, undefined) {
childNodes: [] childNodes: []
} }
var nodeCache = [], cellCache = {} var nodeCache = [], cellCache = {}
m.render = function(root, cell) { m.render = function(root, cell, forceRecreation) {
var configs = [] var configs = []
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.") if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.")
var id = getCellCacheKey(root) var id = getCellCacheKey(root)
var node = root == window.document || root == window.document.documentElement ? documentNode : root var node = root == window.document || root == window.document.documentElement ? documentNode : root
if (cellCache[id] === undefined) clear(node.childNodes) if (cellCache[id] === undefined) clear(node.childNodes)
if (forceRecreation === true) reset(root)
cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs) cellCache[id] = build(node, null, undefined, undefined, cell, cellCache[id], false, 0, null, undefined, configs)
for (var i = 0; i < configs.length; i++) configs[i]() for (var i = 0; i < configs.length; i++) configs[i]()
} }
@ -359,6 +361,17 @@ Mithril = m = new function app(window, undefined) {
return value return value
} }
m.prop = function(store) {
var prop = function() {
if (arguments.length) store = arguments[0]
return store
}
prop.toJSON = function() {
return store
}
return prop
}
var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false var roots = [], modules = [], controllers = [], lastRedrawId = 0, computePostRedrawHook = null, prevented = false
m.module = function(root, module) { m.module = function(root, module) {
var index = roots.indexOf(root) var index = roots.indexOf(root)
@ -371,6 +384,7 @@ Mithril = m = new function app(window, undefined) {
controllers[index].onunload(event) controllers[index].onunload(event)
} }
if (!isPrevented) { if (!isPrevented) {
m.redraw.strategy("all")
m.startComputation() m.startComputation()
roots[index] = root roots[index] = root
modules[index] = module modules[index] = module
@ -390,15 +404,18 @@ Mithril = m = new function app(window, undefined) {
lastRedrawId = defer(function() {lastRedrawId = null}, 0) lastRedrawId = defer(function() {lastRedrawId = null}, 0)
} }
} }
m.redraw.strategy = m.prop()
function redraw() { function redraw() {
var mode = m.redraw.strategy()
for (var i = 0; i < roots.length; i++) { for (var i = 0; i < roots.length; i++) {
if (controllers[i]) m.render(roots[i], modules[i].view(controllers[i])) if (controllers[i] && mode != "none") m.render(roots[i], modules[i].view(controllers[i]), mode == "all")
} }
if (computePostRedrawHook) { if (computePostRedrawHook) {
computePostRedrawHook() computePostRedrawHook()
computePostRedrawHook = null computePostRedrawHook = null
} }
lastRedrawId = null lastRedrawId = null
m.redraw.strategy("diff")
} }
var pendingRequests = 0 var pendingRequests = 0
@ -480,7 +497,6 @@ Mithril = m = new function app(window, undefined) {
for (var route in router) { for (var route in router) {
if (route == path) { if (route == path) {
reset(root)
m.module(root, router[route]) m.module(root, router[route])
return true return true
} }
@ -488,7 +504,6 @@ Mithril = m = new function app(window, undefined) {
var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$") var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "\/?$")
if (matcher.test(path)) { if (matcher.test(path)) {
reset(root)
path.replace(matcher, function() { path.replace(matcher, function() {
var keys = route.match(/:[^\/]+/g) || [] var keys = route.match(/:[^\/]+/g) || []
var values = [].slice.call(arguments, 1, -2) var values = [].slice.call(arguments, 1, -2)
@ -499,11 +514,6 @@ Mithril = m = new function app(window, undefined) {
} }
} }
} }
function reset(root) {
var cacheKey = getCellCacheKey(root)
clear(root.childNodes, cellCache[cacheKey])
cellCache[cacheKey] = undefined
}
function routeUnobtrusive(e) { function routeUnobtrusive(e) {
e = e || event e = e || event
if (e.ctrlKey || e.metaKey || e.which == 2) return if (e.ctrlKey || e.metaKey || e.which == 2) return
@ -533,17 +543,10 @@ Mithril = m = new function app(window, undefined) {
function decodeSpace(string) { function decodeSpace(string) {
return decodeURIComponent(string.replace(/\+/g, " ")) return decodeURIComponent(string.replace(/\+/g, " "))
} }
function reset(root) {
//model var cacheKey = getCellCacheKey(root)
m.prop = function(store) { clear(root.childNodes, cellCache[cacheKey])
var prop = function() { cellCache[cacheKey] = undefined
if (arguments.length) store = arguments[0]
return store
}
prop.toJSON = function() {
return store
}
return prop
} }
var none = {} var none = {}

View file

@ -1298,6 +1298,101 @@ function testMithril(mock) {
mock.requestAnimationFrame.$resolve() //teardown mock.requestAnimationFrame.$resolve() //teardown
return unloaded == 1 return unloaded == 1
}) })
test(function() {
mock.requestAnimationFrame.$resolve() //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var strategy
m.route.mode = "search"
m.route(root, "/foo1", {
"/foo1": {
controller: function() {
strategy = m.redraw.strategy()
m.redraw.strategy("none")
},
view: function() {
return m("div");
}
}
})
mock.requestAnimationFrame.$resolve() //teardown
return strategy == "all" && root.childNodes.length == 0
})
test(function() {
mock.requestAnimationFrame.$resolve() //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var strategy, count = 0
var config = function(el, init) {if (!init) count++}
m.route.mode = "search"
m.route(root, "/foo1", {
"/foo1": {
controller: function() {},
view: function() {
return m("div", {config: config});
}
},
"/bar1": {
controller: function() {
strategy = m.redraw.strategy()
m.redraw.strategy("redraw")
},
view: function() {
return m("div", {config: config});
}
},
})
mock.requestAnimationFrame.$resolve()
m.route("/bar1")
mock.requestAnimationFrame.$resolve() //teardown
return strategy == "all" && count == 1
})
test(function() {
mock.requestAnimationFrame.$resolve() //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var strategy
m.route.mode = "search"
m.route(root, "/foo1", {
"/foo1": {
controller: function() {this.number = 1},
view: function(ctrl) {
return m("div", {onclick: function() {
strategy = m.redraw.strategy()
ctrl.number++
m.redraw.strategy("none")
}}, ctrl.number);
}
}
})
root.childNodes[0].onclick({})
return strategy == "diff" && root.childNodes[0].childNodes[0].nodeValue == "1"
})
test(function() {
mock.requestAnimationFrame.$resolve() //setup
mock.location.search = "?"
var root = mock.document.createElement("div")
var count = 0
var config = function(el, init ) {if (!init) count++}
m.route.mode = "search"
m.route(root, "/foo1", {
"/foo1": {
controller: function() {},
view: function(ctrl) {
return m("div", {config: config, onclick: function() {
m.redraw.strategy("all")
}});
}
}
})
root.childNodes[0].onclick({})
mock.requestAnimationFrame.$resolve() //teardown
return count == 2
})
//end m.route //end m.route
//m.prop //m.prop