add {subtree: "retain"} flag to allow skipping diff from app space

This commit is contained in:
Leo Horie 2014-04-06 15:20:21 -04:00
parent c4494bf2ce
commit fcf77dfa44
12 changed files with 169 additions and 11 deletions

View file

@ -65,6 +65,7 @@
<ul>
<li>diff no longer touch the DOM when processing <code>style</code> attributes and event handlers</li>
<li>returning a thennable to a resolution callback in <code>m.deferred().promise</code> now causes the promise to adopt its state </li>
<li>diff now correctly clears subtree if null or undefined is passed as a node</li>
</ul>
<hr>
<p><a href="/mithril/archive/v0.1.2">v0.1.2</a> - maintenance</p>

View file

@ -33,7 +33,11 @@ new function(window) {
return cell
}
function build(parent, data, cached) {
if (data === null || data === undefined) return
if (data === null || data === undefined) {
if (cached) clear(cached.nodes)
return
}
if (data.subtree === "retain") return
var cachedType = type.call(cached), dataType = type.call(data)
if (cachedType != dataType) {
@ -59,8 +63,8 @@ new function(window) {
}
}
else if (dataType == "[object Object]") {
if (typeof data.tag != "string") return
if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join()) clear(cached.nodes)
if (typeof data.tag != "string") return
var node, isNew = cached.nodes.length === 0
if (isNew) {
@ -640,6 +644,36 @@ function testMithril(mock) {
m.render(root, m("div", [m("a", {href: "/second"})]))
return root.childNodes[0].childNodes.length == 1
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li")]))
m.render(root, m("ul", [m("li"), undefined]))
return root.childNodes[0].childNodes.length === 1
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li"), m("li")]))
m.render(root, m("ul", [m("li"), undefined]))
return root.childNodes[0].childNodes.length === 1
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li")]))
m.render(root, m("ul", [undefined]))
return root.childNodes[0].childNodes.length === 0
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li")]))
m.render(root, m("ul", [{}]))
return root.childNodes[0].childNodes.length === 0
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li", [m("a")])]))
m.render(root, m("ul", [{subtree: "retain"}]))
return root.childNodes[0].childNodes[0].childNodes[0].nodeName === "A"
})
//m.redraw
test(function() {

View file

@ -183,7 +183,8 @@ m(&quot;a[href=&#39;/dashboard&#39;]&quot;, {config: m.route}, &quot;Dashboard&q
where:
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object&lt;any | void config(DOMElement element, Boolean isInitialized)&gt;
Children :: String text | Array&lt;String text | VirtualElement virtualElement | Children children&gt;</code></pre>
Children :: String text | Array&lt;String text | VirtualElement virtualElement | SubtreeDirective directive | Children children&gt;
SubtreeDirective :: Object { String subtree }</code></pre>
<ul>
<li><p><strong>String selector</strong></p>
<p>This string should be a CSS rule that represents a DOM element.</p>
@ -259,6 +260,7 @@ m(&quot;a[href=&#39;/dashboard&#39;]&quot;, {config: m.route}, &quot;Dashboard&q
<p>If this argument is a string, it will be rendered as a text node. To render a string as HTML, see <a href="mithril.trust"><code>m.trust</code></a></p>
<p>If it&#39;s a VirtualElement, it will be rendered as a DOM Element.</p>
<p>If it&#39;s a list, its contents will recursively be rendered as appropriate and appended as children of the element being created.</p>
<p>If it&#39;s a SubtreeDirective with the value &quot;retain&quot;, it will retain the existing DOM tree in place, if any. See <a href="mithril.render#subtree-directives">subtree directives</a> for more information.</p>
</li>
<li><p><strong>returns</strong> VirtualElement</p>
<p>The returned VirtualElement is a javascript data structure that represents the DOM element to be rendered by <a href="mithril.render"><code>m.render</code></a></p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -86,14 +86,50 @@ m.render(document.body, [
&lt;/ul&gt;
&lt;/body&gt;</code></pre>
<hr>
<h3 id="subtree-directives">Subtree Directives</h3>
<p><code>m.render</code> accepts a special low level SubtreeDirective object as a node in a virtual DOM tree: if a tree contains a node that looks exactly like the object below, Mithril will abort the diff algorithm for that node. This allows you to implement optimizations that avoid creating virtual DOM trees in favor of their cached counterparts, if you know they have not changed between redraws. Note that using this feature is discouraged if you don&#39;t have visible performance problems.</p>
<pre><code class="lang-javascript">{subtree: &quot;retain&quot;}</code></pre>
<p>This mechanism is only intended to be used as a last resort optimization tool. If you do use it, you are responsible for determining what constitutes a scenario where the virtual DOM tree is changed/unchanged.</p>
<p>The example below shows how to use a SubtreeDirective object to create a static header that doesn&#39;t incur diff costs once it has been rendered. This means that we are avoiding the creation of the header subtree (and therefore skipping the diff algorithm) altogether, but it also means that dynamic variables will NOT be updated within the header.</p>
<pre><code>var app = {}
//here&#39;s an example plugin that determines whether data has changes.
//in this case, it simply assume data has changed the first time, and never changes after that.
app.bindOnce = new function() {
var cache = {}
function(view) {
if (!cache[view.toString()]) {
cache[view.toString()] = true
return view()
}
else return {subtree: &quot;retain&quot;}
}
}
//here&#39;s the view
app.view = function(ctrl) {
m(&quot;.layout&quot;, [
app.bindOnce(function() {
//this only runs once in order to boost performance
//dynamic variables are not updated here
return m(&quot;header&quot;, [
m(&quot;h1&quot;, &quot;this never changes&quot;)
])
}),
//dynamic variables here still update on every redraw
m(&quot;main&quot;, &quot;rest of app goes here&quot;)
])
}</code></pre>
<hr>
<h3 id="signature">Signature</h3>
<p><a href="how-to-read-signatures.html">How to read signatures</a></p>
<pre><code class="lang-clike">void render(DOMElement rootElement, Children children)
where:
Children :: String text | Array&lt;String text | VirtualElement virtualElement | Children children&gt;
Children :: String text | Array&lt;String text | VirtualElement virtualElement | SubtreeDirective directive | Children children&gt;
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object&lt;Any | void config(DOMElement element)&gt;</code></pre>
Attributes :: Object&lt;Any | void config(DOMElement element)&gt;
SubtreeDirective :: Object { String subtree }</code></pre>
<ul>
<li><p><strong>DOMElement rootElement</strong></p>
<p>A DOM element which will contain the template represented by <code>children</code>.</p>

View file

@ -6,6 +6,7 @@
- diff no longer touch the DOM when processing `style` attributes and event handlers
- returning a thennable to a resolution callback in `m.deferred().promise` now causes the promise to adopt its state
- diff now correctly clears subtree if null or undefined is passed as a node
---

View file

@ -195,7 +195,8 @@ VirtualElement m(String selector [, Attributes attributes] [, Children children]
where:
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object<any | void config(DOMElement element, Boolean isInitialized)>
Children :: String text | Array<String text | VirtualElement virtualElement | Children children>
Children :: String text | Array<String text | VirtualElement virtualElement | SubtreeDirective directive | Children children>
SubtreeDirective :: Object { String subtree }
```
- **String selector**
@ -316,6 +317,8 @@ where:
If it's a VirtualElement, it will be rendered as a DOM Element.
If it's a list, its contents will recursively be rendered as appropriate and appended as children of the element being created.
If it's a SubtreeDirective with the value "retain", it will retain the existing DOM tree in place, if any. See [subtree directives](mithril.render#subtree-directives) for more information.
- **returns** VirtualElement

View file

@ -40,6 +40,52 @@ yields:
---
### Subtree Directives
`m.render` accepts a special low level SubtreeDirective object as a node in a virtual DOM tree: if a tree contains a node that looks exactly like the object below, Mithril will abort the diff algorithm for that node. This allows you to implement optimizations that avoid creating virtual DOM trees in favor of their cached counterparts, if you know they have not changed between redraws. Note that using this feature is discouraged if you don't have visible performance problems.
```javascript
{subtree: "retain"}
```
This mechanism is only intended to be used as a last resort optimization tool. If you do use it, you are responsible for determining what constitutes a scenario where the virtual DOM tree is changed/unchanged.
The example below shows how to use a SubtreeDirective object to create a static header that doesn't incur diff costs once it has been rendered. This means that we are avoiding the creation of the header subtree (and therefore skipping the diff algorithm) altogether, but it also means that dynamic variables will NOT be updated within the header.
```
var app = {}
//here's an example plugin that determines whether data has changes.
//in this case, it simply assume data has changed the first time, and never changes after that.
app.bindOnce = new function() {
var cache = {}
function(view) {
if (!cache[view.toString()]) {
cache[view.toString()] = true
return view()
}
else return {subtree: "retain"}
}
}
//here's the view
app.view = function(ctrl) {
m(".layout", [
app.bindOnce(function() {
//this only runs once in order to boost performance
//dynamic variables are not updated here
return m("header", [
m("h1", "this never changes")
])
}),
//dynamic variables here still update on every redraw
m("main", "rest of app goes here")
])
}
```
---
### Signature
[How to read signatures](how-to-read-signatures.md)
@ -48,9 +94,10 @@ yields:
void render(DOMElement rootElement, Children children)
where:
Children :: String text | Array<String text | VirtualElement virtualElement | Children children>
Children :: String text | Array<String text | VirtualElement virtualElement | SubtreeDirective directive | Children children>
VirtualElement :: Object { String tag, Attributes attributes, Children children }
Attributes :: Object<Any | void config(DOMElement element)>
SubtreeDirective :: Object { String subtree }
```
- **DOMElement rootElement**

View file

@ -33,7 +33,11 @@ new function(window) {
return cell
}
function build(parent, data, cached) {
if (data === null || data === undefined) return
if (data === null || data === undefined) {
if (cached) clear(cached.nodes)
return
}
if (data.subtree === "retain") return
var cachedType = type.call(cached), dataType = type.call(data)
if (cachedType != dataType) {
@ -59,8 +63,8 @@ new function(window) {
}
}
else if (dataType == "[object Object]") {
if (typeof data.tag != "string") return
if (data.tag != cached.tag || Object.keys(data.attrs).join() != Object.keys(cached.attrs).join()) clear(cached.nodes)
if (typeof data.tag != "string") return
var node, isNew = cached.nodes.length === 0
if (isNew) {

View file

@ -111,6 +111,36 @@ function testMithril(mock) {
m.render(root, m("div", [m("a", {href: "/second"})]))
return root.childNodes[0].childNodes.length == 1
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li")]))
m.render(root, m("ul", [m("li"), undefined]))
return root.childNodes[0].childNodes.length === 1
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li"), m("li")]))
m.render(root, m("ul", [m("li"), undefined]))
return root.childNodes[0].childNodes.length === 1
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li")]))
m.render(root, m("ul", [undefined]))
return root.childNodes[0].childNodes.length === 0
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li")]))
m.render(root, m("ul", [{}]))
return root.childNodes[0].childNodes.length === 0
})
test(function() {
var root = mock.document.createElement("div")
m.render(root, m("ul", [m("li", [m("a")])]))
m.render(root, m("ul", [{fromCache: true}]))
return root.childNodes[0].childNodes[0].childNodes[0].nodeName === "A"
})
//m.redraw
test(function() {