shouldUpdate, component oninit order fix
This commit is contained in:
parent
875984c177
commit
ead2e8ac0b
10 changed files with 329 additions and 82 deletions
|
|
@ -6,13 +6,13 @@ This rewrite aims to fix longstanding API design issues, significantly improve p
|
|||
|
||||
## Status
|
||||
|
||||
Code still is in flux. Most notably, components and thunks (`{subtree: "retain"}`) are currently not implemented yet and there are several use cases that still need to be polished. DO NOT USE IN PRODUCTION YET!
|
||||
Code still is in flux. Most notably, thunks (`{subtree: "retain"}`) are currently not implemented yet and there are several use cases that still need to be polished. DO NOT USE IN PRODUCTION YET!
|
||||
|
||||
Some examples of usage can be found in the [examples](examples) folder. [ThreadItJS](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/threaditjs/index.html) has the largest API surface coverage and comments indicating pending issues in framework usability. Note that the APIs those examples use may not become the final public API points in v1.0.
|
||||
|
||||
## Performance
|
||||
|
||||
Mithril's virtual DOM engine is now less than 400 lines of well organized code and it implements a modern search space reduction diff algorithm and a DOM recycling mechanism, which translate to top-of-class performance. See the [dbmon implementation (non-optimized)](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) (for comparison, here are optimized dbmon implementations for [React v15.0.2](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and [Angular v2.0.0-beta.17](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html)).
|
||||
Mithril's virtual DOM engine is now around 400 lines of well organized code and it implements a modern search space reduction diff algorithm and a DOM recycling mechanism, which translate to top-of-class performance. See the [dbmon implementation (non-optimized)](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/mithril/index.html) (for comparison, here are optimized dbmon implementations for [React v15.0.2](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/react/index.html) and [Angular v2.0.0-beta.17](http://cdn.rawgit.com/lhorie/mithril.js/rewrite/examples/dbmonster/angular/index.html)).
|
||||
|
||||
## Lifecycle methods and Animation Support
|
||||
|
||||
|
|
|
|||
2
examples/dbmonster/angular/app.js
vendored
2
examples/dbmonster/angular/app.js
vendored
|
|
@ -14,7 +14,7 @@ var AppComponent = ng.core.Component({selector: "my-app"})
|
|||
"<td class='query-count'>" +
|
||||
"<span [class]='db.lastSample.countClassName'>{{db.lastSample.nbQueries}}</span>" +
|
||||
"</td>" +
|
||||
"<td *ngFor='let q of db.lastSample.topFiveQueries' [class]='q.elapsedClassName'>" +
|
||||
"<td *ngFor='let q of db.lastSample.topFiveQueries' [class]='\"Query \" + q.elapsedClassName'>" +
|
||||
"{{q.formatElapsed}}" +
|
||||
"<div class='popover left'>" +
|
||||
"<div class='popover-content'>{{q.query}}</div>" +
|
||||
|
|
|
|||
72
index.html
72
index.html
|
|
@ -1,72 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<style>* {font-size:40px;}</style>
|
||||
</head>
|
||||
<body>
|
||||
<script src="./module/module.js"></script>
|
||||
<script src="./render/node.js"></script>
|
||||
<script src="./render/hyperscript.js"></script>
|
||||
<script src="./render/render.js"></script>
|
||||
<script>
|
||||
var m = require("./render/hyperscript")
|
||||
var render = require("./render/render")(window, run).render
|
||||
|
||||
var count = 10
|
||||
function inc() {count++}
|
||||
|
||||
var Counter1 = {
|
||||
oninit: function(vnode) {console.log("init", vnode)},
|
||||
oncreate: function(vnode) {console.log("create", vnode)},
|
||||
onupdate: function(vnode) {console.log("update", vnode)},
|
||||
onremove: function(vnode) {console.log("remove", vnode)},
|
||||
onbeforeremove: function(vnode, done) {console.log("before remove", vnode);done()},
|
||||
view: function({attrs, state}) {
|
||||
return m("a", {
|
||||
onclick: () => {
|
||||
count++
|
||||
state.visible = !state.visible
|
||||
}
|
||||
}, attrs.count + (state.visible ? "visible" : ""))
|
||||
}
|
||||
}
|
||||
/*
|
||||
class Counter2 {
|
||||
oninit(vnode) {
|
||||
request().then(function(foo) {
|
||||
vnode.state.foo = foo
|
||||
})
|
||||
}
|
||||
onupdate(vnode) {
|
||||
vnode.dom.style.borderRight = vnode.attrs.count + "px solid red"
|
||||
}
|
||||
view(vnode) {
|
||||
return m("a", {onclick: inc}, vnode.attrs.count)
|
||||
}
|
||||
}
|
||||
function Counter3() {
|
||||
return m("a", {
|
||||
onupdate: function(vnode) {
|
||||
vnode.dom.style.borderRight = vnode.attrs.count + "px solid red"
|
||||
},
|
||||
onclick: inc
|
||||
}, vnode.attrs.count)
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
function view() {
|
||||
return m("div", [
|
||||
m("a", {onclick: inc}, count),
|
||||
m("br"),
|
||||
count % 7 !== 0 ? {tag: Counter1, attrs: {count: count}, text: "test", state: {}} : null,
|
||||
])
|
||||
}
|
||||
|
||||
function run() {
|
||||
render(document.body, view())
|
||||
}
|
||||
run()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -189,7 +189,7 @@ module.exports = new function init() {
|
|||
for (var i = 0, r; r = results[i]; i++) {
|
||||
if (!r.pass) console.error(r.context + ": " + highlight(r.message) + "\n\n" + r.error.match(/^(?:(?!Error|[\/\\]ospec[\/\\]ospec\.js).)*$/m) + "\n\n", hasProcess ? "" : "color:red", hasProcess ? "" : "color:black")
|
||||
}
|
||||
console.log(results.length + " tests completed in " + Math.round(new Date - start) + "ms")
|
||||
console.log(results.length + " assertions completed in " + Math.round(new Date - start) + "ms")
|
||||
}
|
||||
|
||||
return o
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ function hyperscript(selector) {
|
|||
}
|
||||
|
||||
if (typeof selector === "string") return selectorCache[selector](attrs || {}, Node.normalizeChildren(children))
|
||||
return Node(selector, attrs && attrs.key, attrs, Node.normalizeChildren(children), undefined, undefined)
|
||||
return Node(selector, attrs && attrs.key, attrs || {}, Node.normalizeChildren(children), undefined, undefined)
|
||||
}
|
||||
|
||||
function changeNS(ns, vnode) {
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ module.exports = function($window, onevent) {
|
|||
return element
|
||||
}
|
||||
function createComponent(vnode, hooks) {
|
||||
vnode.instance = Node.normalize(vnode.tag.view(vnode))
|
||||
initLifecycle(vnode.tag, vnode, hooks)
|
||||
vnode.instance = Node.normalize(vnode.tag.view.call(vnode, vnode))
|
||||
var element = createNode(vnode.instance, hooks)
|
||||
vnode.dom = vnode.instance.dom
|
||||
vnode.domSize = vnode.instance.domSize
|
||||
|
|
@ -161,7 +161,10 @@ module.exports = function($window, onevent) {
|
|||
var oldTag = old.tag, tag = vnode.tag
|
||||
if (oldTag === tag) {
|
||||
vnode.state = old.state
|
||||
if (vnode.attrs != null) updateLifecycle(vnode.attrs, vnode, hooks, recycling)
|
||||
if (shouldUpdate(vnode, old)) return
|
||||
if (vnode.attrs != null) {
|
||||
updateLifecycle(vnode.attrs, vnode, hooks, recycling)
|
||||
}
|
||||
if (typeof oldTag === "string") {
|
||||
switch (oldTag) {
|
||||
case "#": updateText(old, vnode); break
|
||||
|
|
@ -218,7 +221,7 @@ module.exports = function($window, onevent) {
|
|||
}
|
||||
}
|
||||
function updateComponent(parent, old, vnode, hooks, nextSibling, recycling) {
|
||||
vnode.instance = Node.normalize(vnode.tag.view(vnode))
|
||||
vnode.instance = Node.normalize(vnode.tag.view.call(vnode, vnode))
|
||||
updateLifecycle(vnode.tag, vnode, hooks, recycling)
|
||||
updateNode(parent, old.instance, vnode.instance, hooks, nextSibling, recycling)
|
||||
vnode.dom = vnode.instance.dom
|
||||
|
|
@ -373,7 +376,7 @@ module.exports = function($window, onevent) {
|
|||
}
|
||||
}
|
||||
function isLifecycleMethod(attr) {
|
||||
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove"
|
||||
return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "shouldUpdate"
|
||||
}
|
||||
function isAttribute(attr) {
|
||||
return attr === "href" || attr === "list" || attr === "form"// || attr === "type" || attr === "width" || attr === "height"
|
||||
|
|
@ -405,6 +408,18 @@ module.exports = function($window, onevent) {
|
|||
if (recycling) initLifecycle(source, vnode, hooks)
|
||||
else if (source.onupdate != null) hooks.push(source.onupdate.bind(vnode, vnode))
|
||||
}
|
||||
function shouldUpdate(vnode, old) {
|
||||
var forceVnodeUpdate, forceComponentUpdate
|
||||
if (vnode.attrs != null && typeof vnode.attrs.shouldUpdate === "function") forceVnodeUpdate = vnode.attrs.shouldUpdate(vnode, old)
|
||||
if (typeof vnode.tag !== "string" && typeof vnode.tag.shouldUpdate === "function") forceComponentUpdate = vnode.tag.shouldUpdate(vnode, old)
|
||||
if (!(forceVnodeUpdate === undefined && forceComponentUpdate === undefined) && !forceVnodeUpdate && !forceComponentUpdate) {
|
||||
vnode.dom = old.dom
|
||||
vnode.domSize = old.domSize
|
||||
vnode.instance = old.instance
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function render(dom, vnodes) {
|
||||
var hooks = []
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
<script src="test-onupdate.js"></script>
|
||||
<script src="test-onremove.js"></script>
|
||||
<script src="test-onbeforeremove.js"></script>
|
||||
<script src="test-shouldUpdate.js"></script>
|
||||
<script src="test-attributes.js"></script>
|
||||
<script src="test-event.js"></script>
|
||||
<script src="test-input.js"></script>
|
||||
|
|
|
|||
|
|
@ -257,6 +257,21 @@ o.spec("component", function() {
|
|||
o(root.firstChild.attributes["id"].nodeValue).equals("a")
|
||||
o(root.firstChild.firstChild.nodeValue).equals("b")
|
||||
})
|
||||
o("calls oninit before view", function() {
|
||||
var viewCalled = false
|
||||
|
||||
render(root, {
|
||||
tag: {
|
||||
view: function() {
|
||||
viewCalled = true
|
||||
return [{tag: "div", attrs: {id: "a"}, text: "b"}]
|
||||
},
|
||||
oninit: function(vnode) {
|
||||
o(viewCalled).equals(false)
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
o("calls oncreate", function() {
|
||||
var called = 0
|
||||
var component = {
|
||||
|
|
|
|||
289
render/tests/test-shouldUpdate.js
Normal file
289
render/tests/test-shouldUpdate.js
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
"use strict"
|
||||
|
||||
var o = require("../../ospec/ospec")
|
||||
var domMock = require("../../test-utils/domMock")
|
||||
var vdom = require("../../render/render")
|
||||
|
||||
o.spec("shouldUpdate", function() {
|
||||
var $window, root, render
|
||||
o.beforeEach(function() {
|
||||
$window = domMock()
|
||||
root = $window.document.createElement("div")
|
||||
render = vdom($window).render
|
||||
})
|
||||
|
||||
o("prevents update in element", function() {
|
||||
var shouldUpdate = function() {return false}
|
||||
var vnode = {tag: "div", attrs: {id: "a", shouldUpdate: shouldUpdate}}
|
||||
var updated = {tag: "div", attrs: {id: "b", shouldUpdate: shouldUpdate}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("a")
|
||||
})
|
||||
|
||||
o("prevents update in text", function() {
|
||||
var shouldUpdate = function() {return false}
|
||||
var vnode = {tag: "#", attrs: {shouldUpdate: shouldUpdate}, children: "a"}
|
||||
var updated = {tag: "#", attrs: {shouldUpdate: shouldUpdate}, children: "b"}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.nodeValue).equals("a")
|
||||
})
|
||||
|
||||
o("prevents update in html", function() {
|
||||
var shouldUpdate = function() {return false}
|
||||
var vnode = {tag: "<", attrs: {shouldUpdate: shouldUpdate}, children: "a"}
|
||||
var updated = {tag: "<", attrs: {shouldUpdate: shouldUpdate}, children: "b"}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.nodeValue).equals("a")
|
||||
})
|
||||
|
||||
o("prevents update in fragment", function() {
|
||||
var shouldUpdate = function() {return false}
|
||||
var vnode = {tag: "[", attrs: {shouldUpdate: shouldUpdate}, children: [{tag: "#", children: "a"}]}
|
||||
var updated = {tag: "[", attrs: {shouldUpdate: shouldUpdate}, children: [{tag: "#", children: "b"}]}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.nodeValue).equals("a")
|
||||
})
|
||||
|
||||
o("prevents update in component", function() {
|
||||
var component = {
|
||||
shouldUpdate: function() {return false},
|
||||
view: function(vnode) {
|
||||
return {tag: "div", children: vnode.children}
|
||||
},
|
||||
}
|
||||
var vnode = {tag: component, children: [{tag: "#", children: "a"}]}
|
||||
var updated = {tag: component, children: [{tag: "#", children: "b"}]}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.firstChild.nodeValue).equals("a")
|
||||
})
|
||||
|
||||
o("prevents update if returning false in component and false in vnode", function() {
|
||||
var component = {
|
||||
shouldUpdate: function() {return false},
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: {id: vnode.attrs.id}}
|
||||
},
|
||||
}
|
||||
var vnode = {tag: component, attrs: {id: "a", shouldUpdate: function() {return false}}}
|
||||
var updated = {tag: component, attrs: {id: "b", shouldUpdate: function() {return false}}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("a")
|
||||
})
|
||||
|
||||
o("does not prevent update if returning true in component and true in vnode", function() {
|
||||
var component = {
|
||||
shouldUpdate: function() {return true},
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: {id: vnode.attrs.id}}
|
||||
},
|
||||
}
|
||||
var vnode = {tag: component, attrs: {id: "a", shouldUpdate: function() {return true}}}
|
||||
var updated = {tag: component, attrs: {id: "b", shouldUpdate: function() {return true}}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("does not prevent update if returning false in component but true in vnode", function() {
|
||||
var component = {
|
||||
shouldUpdate: function() {return false},
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: {id: vnode.attrs.id}}
|
||||
},
|
||||
}
|
||||
var vnode = {tag: component, attrs: {id: "a", shouldUpdate: function() {return true}}}
|
||||
var updated = {tag: component, attrs: {id: "b", shouldUpdate: function() {return true}}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("does not prevent update if returning true in component but false in vnode", function() {
|
||||
var component = {
|
||||
shouldUpdate: function() {return true},
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: {id: vnode.attrs.id}}
|
||||
},
|
||||
}
|
||||
var vnode = {tag: component, attrs: {id: "a", shouldUpdate: function() {return false}}}
|
||||
var updated = {tag: component, attrs: {id: "b", shouldUpdate: function() {return false}}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("does not prevent update if returning true", function() {
|
||||
var shouldUpdate = function() {return true}
|
||||
var vnode = {tag: "div", attrs: {id: "a", shouldUpdate: shouldUpdate}}
|
||||
var updated = {tag: "div", attrs: {id: "b", shouldUpdate: shouldUpdate}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("does not prevent update if returning true from component", function() {
|
||||
var component = {
|
||||
shouldUpdate: function() {return true},
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: vnode.attrs}
|
||||
},
|
||||
}
|
||||
var vnode = {tag: component, attrs: {id: "a"}}
|
||||
var updated = {tag: component, attrs: {id: "b"}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("accepts arguments for comparison", function() {
|
||||
var count = 0
|
||||
var vnode = {tag: "div", attrs: {id: "a", shouldUpdate: shouldUpdate}}
|
||||
var updated = {tag: "div", attrs: {id: "b", shouldUpdate: shouldUpdate}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
function shouldUpdate(vnode, old) {
|
||||
count++
|
||||
|
||||
o(old.attrs.id).equals("a")
|
||||
o(vnode.attrs.id).equals("b")
|
||||
|
||||
return old.attrs.id !== vnode.attrs.id
|
||||
}
|
||||
|
||||
o(count).equals(1)
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("accepts arguments for comparison in component", function() {
|
||||
var component = {
|
||||
shouldUpdate: shouldUpdate,
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: vnode.attrs}
|
||||
},
|
||||
}
|
||||
var count = 0
|
||||
var vnode = {tag: component, attrs: {id: "a"}}
|
||||
var updated = {tag: component, attrs: {id: "b"}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
function shouldUpdate(vnode, old) {
|
||||
count++
|
||||
|
||||
o(old.attrs.id).equals("a")
|
||||
o(vnode.attrs.id).equals("b")
|
||||
|
||||
return old.attrs.id !== vnode.attrs.id
|
||||
}
|
||||
|
||||
o(count).equals(1)
|
||||
o(root.firstChild.attributes["id"].nodeValue).equals("b")
|
||||
})
|
||||
|
||||
o("is not called on creation", function() {
|
||||
var count = 0
|
||||
var vnode = {tag: "div", attrs: {id: "a", shouldUpdate: shouldUpdate}}
|
||||
var updated = {tag: "div", attrs: {id: "b", shouldUpdate: shouldUpdate}}
|
||||
|
||||
render(root, [vnode])
|
||||
|
||||
function shouldUpdate(vnode, old) {
|
||||
count++
|
||||
return true
|
||||
}
|
||||
|
||||
o(count).equals(0)
|
||||
})
|
||||
|
||||
o("is not called on component creation", function() {
|
||||
var component = {
|
||||
shouldUpdate: shouldUpdate,
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: vnode.attrs}
|
||||
},
|
||||
}
|
||||
|
||||
var count = 0
|
||||
var vnode = {tag: "div", attrs: {id: "a"}}
|
||||
var updated = {tag: "div", attrs: {id: "b"}}
|
||||
|
||||
render(root, [vnode])
|
||||
|
||||
function shouldUpdate(vnode, old) {
|
||||
count++
|
||||
return true
|
||||
}
|
||||
|
||||
o(count).equals(0)
|
||||
})
|
||||
|
||||
o("is called only once on update", function() {
|
||||
var count = 0
|
||||
var vnode = {tag: "div", attrs: {id: "a", shouldUpdate: shouldUpdate}}
|
||||
var updated = {tag: "div", attrs: {id: "b", shouldUpdate: shouldUpdate}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
function shouldUpdate(vnode, old) {
|
||||
count++
|
||||
return true
|
||||
}
|
||||
|
||||
o(count).equals(1)
|
||||
})
|
||||
|
||||
o("is called only once on component update", function() {
|
||||
var component = {
|
||||
shouldUpdate: shouldUpdate,
|
||||
view: function(vnode) {
|
||||
return {tag: "div", attrs: vnode.attrs}
|
||||
},
|
||||
}
|
||||
|
||||
var count = 0
|
||||
var vnode = {tag: component, attrs: {id: "a"}}
|
||||
var updated = {tag: component, attrs: {id: "b"}}
|
||||
|
||||
render(root, [vnode])
|
||||
render(root, [updated])
|
||||
|
||||
function shouldUpdate(vnode, old) {
|
||||
count++
|
||||
return true
|
||||
}
|
||||
|
||||
o(count).equals(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
"use strict"
|
||||
|
||||
var o = require("../../ospec/ospec")
|
||||
var domMock = require("../../test-utils/domMock")
|
||||
var trust = require("../../render/trust")
|
||||
|
||||
o.spec("trust", function() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue