Updated redrawing algorithm to minimize number of DOM operations.
@@ -32,7 +32,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -91,7 +90,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: "test",@@ -102,7 +100,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -123,7 +120,6 @@ classList: ["a", "b", "c"],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "div",@@ -141,7 +137,6 @@ data: {},
attributes: {}, eventListeners: {}, id: "test", - $key: undefined, $html: undefined, style: undefined, type: "div",@@ -160,7 +155,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -173,7 +167,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -185,7 +178,6 @@ classList: ["test"],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -205,7 +197,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -218,7 +209,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -228,7 +218,6 @@ ],
data: {}, eventListeners: {}, id: "test", - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -244,7 +233,6 @@ children: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: "AAA",@@ -263,7 +251,6 @@ eventListeners: {
click: fn, }, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -289,7 +276,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -305,7 +291,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -317,7 +302,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -332,7 +316,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, type: "#text",@@ -344,7 +327,6 @@ classList: [],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,@@ -354,7 +336,6 @@ classList: ["test"],
data: {}, eventListeners: {}, id: undefined, - $key: undefined, $html: undefined, style: undefined, value: undefined,
@@ -7,7 +7,6 @@ const obj = {
id: "test", type: "input", value: "AAA", - $key: "123", $html: "", data: { a: "1", b: "2" }, eventListeners: { click: fn },@@ -20,7 +19,6 @@ const vnode1 = h3("br");
vnode1.from(obj); const vnode2 = h3("input#test.a1.a2", { value: "AAA", - $key: "123", $html: "", data: { a: "1", b: "2" }, onclick: fn,@@ -336,10 +334,11 @@ expect(n.classList.value).toEqual("vn2");
const vn3 = h3("ul", [h3("span.vn3", { $onrender })]); vn1.redraw({ node: n1, vnode: vn3 }); expect(n.classList.value).toEqual("vn3"); - vn2.render(); - expect(n.classList.value).toEqual("vn2"); const rc = () => h3("div.rc", { $onrender }); await h3.init(rc); expect(n.classList.value).toEqual("rc"); + const rc2 = () => vn2; + await h3.init(rc2); + expect(n.classList.value).toEqual("vn2"); }); });
@@ -7884,7 +7884,6 @@ <ul>
<li>Any attribute starting with <em>on</em> (e.g. onclick, onkeydown, etc.) will be treated as an event listener.</li> <li>The <code>classList</code> attribute can be set to a list of classes to apply to the element (as an alternative to using the element selector shorthand).</li> <li>The <code>data</code> attribute can be set to a simple object containing <a href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes">data attributes</a>.</li> -<li>The special <code>$key</code> attribute can be used to guarantee the uniqueness of two VNodes and it will not be translated into an HTML attribute.</li> <li>The special <code>$html</code> attribute can be used to set the <code>innerHTML</code> property of the resulting HTML element. Use only if you know what you are doing!</li> <li>The special <code>$onrender</code> attribute can be set to a function that will executed every time the VNode is rendered and added to the DOM.</li> </ul>@@ -8051,7 +8050,7 @@ </ul>
</div> <div id="footer"> - <p><span class="copy"></span> Fabio Cevasco – June 8, 2020</p> + <p><span class="copy"></span> Fabio Cevasco – June 9, 2020</p> <p><span>Powered by</span> <a href="https://h3rald.com/hastyscribe"><span class="hastyscribe"></span></a></p> </div> </div>
@@ -2,18 +2,18 @@ import h3 from "../h3.js";
export default function Todo(data) { const todoStateClass = data.done ? ".done" : ".todo"; - const toggleTodo = (todo) => { - h3.dispatch("todos/toggle", data); - h3.redraw() + const toggleTodo = (key) => { + h3.dispatch("todos/toggle", key); + h3.redraw(); }; - const removeTodo = (todo) => { - h3.dispatch("todos/remove", data); - h3.redraw() + const removeTodo = (key) => { + h3.dispatch("todos/remove", key); + h3.redraw(); }; - return h3(`div.todo-item`, {$key: data.key}, [ + return h3(`div.todo-item`, { data: { key: data.key } }, [ h3(`div.todo-content${todoStateClass}`, [ - h3("span.todo-text", { onclick: () => toggleTodo(data) }, data.text), + h3("span.todo-text", { onclick: (e) => toggleTodo(e.currentTarget.parentNode.parentNode.dataset.key) }, data.text), ]), - h3("span.delete-todo", { onclick: () => removeTodo(data) }, "✘"), + h3("span.delete-todo", { onclick: (e) => removeTodo(e.currentTarget.parentNode.dataset.key) }, "✘"), ]); }
@@ -61,6 +61,8 @@ };
const selectorRegex = /^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; +let $onrenderCallbacks = []; + // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) {@@ -68,7 +70,6 @@ this.type = undefined;
this.attributes = {}; this.data = {}; this.id = undefined; - this.$key = undefined; this.$html = undefined; this.$onrender = undefined; this.style = undefined;@@ -158,7 +159,6 @@ from(data) {
this.value = data.value; this.type = data.type; this.id = data.id; - this.$key = data.$key; this.$html = data.$html; this.$onrender = data.$onrender; this.style = data.style;@@ -176,7 +176,6 @@ }
processProperties(attrs) { this.id = this.id || attrs.id; - this.$key = attrs.$key; this.$html = attrs.$html; this.$onrender = attrs.$onrender; this.style = attrs.style;@@ -199,7 +198,6 @@ this.eventListeners[key.slice(2)] = attrs[key];
delete this.attributes[key]; }); delete this.attributes.value; - delete this.attributes.$key; delete this.attributes.$html; delete this.attributes.$onrender; delete this.attributes.id;@@ -301,7 +299,7 @@ // Children
this.children.forEach((c) => { const cnode = c.render(); node.appendChild(cnode); - c.$onrender && c.$onrender(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); }); if (this.$html) { node.innerHTML = this.$html;@@ -327,8 +325,6 @@ newvnode.$onrender && newvnode.$onrender(renderedNode);
oldvnode.from(newvnode); return; } - // $key - oldvnode.$key = newvnode.$key; // ID if (oldvnode.id !== newvnode.id) { node.id = newvnode.id || "";@@ -416,41 +412,67 @@ });
oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - const maxLength = Math.max( - oldvnode.children.length, - newvnode.children.length - ); let map = []; + let oldNodesFound = 0; + let newNodesFound = 0; + // First look for existing nodes for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - if (oldIndex >= newvnode.children.length) { - // not found in new node, remove from old - map.push(-3); - } else { - let found = -1; - for (let index = 0; index < oldvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - break; - } + let found = -1; + for (let index = 0; index < newvnode.children.length; index++) { + if ( + equal(oldvnode.children[oldIndex], newvnode.children[index]) && + !map.includes(index) + ) { + found = index; + newNodesFound++; + oldNodesFound++; + break; } - map.push(found); } + map.push(found); } - // other nodes are new, needs to be added - if (maxLength > oldvnode.children.length) { - map = map.concat( - [...Array(maxLength - oldvnode.children.length)].map(() => -2) - ); + if ( + newNodesFound === oldNodesFound && + newvnode.children.length === oldvnode.children.length + ) { + // something changed but everything else is the same + return map; + } + if (newNodesFound === newvnode.children.length) { + // All children in newvnode exist in oldvnode + // All nodes that are not found must be removed + for (let i = 0; i < map.length; i++) { + if (map[i] === -1) { + map[i] = -3; + } + } + } + if (oldNodesFound === oldvnode.children.length) { + // All children in oldvnode exist in newvnode + // Check where the missing newvnodes children need to be added + for ( + let newIndex = 0; + newIndex < newvnode.children.length; + newIndex++ + ) { + if (!map.includes(newIndex)) { + map.splice(newIndex, 0, -2); + } + } + } + // Check if nodes needs to be removed (if there are fewer children) + if (newvnode.children.length < oldvnode.children.length) { + for (let i = 0; i < map.length; i++) { + if (map[i] === -1 && !newvnode.children[i]) { + map[i] = -3; + } + } } return map; } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.length).keys()]; + let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; for (let i of childMap) {@@ -470,9 +492,9 @@ });
break; case -2: // add node - oldvnode.children.push(newvnode.children[count]); + oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); - node.appendChild(renderedNode); + node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); breakFor = true;@@ -642,6 +664,8 @@ const vnode = newRouteComponent(newRouteComponent.state);
const node = vnode.render(); this.element.appendChild(node); vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state); window.scrollTo(0, 0); this.store.dispatch("$redraw");
@@ -3,7 +3,9 @@
const app = () => { h3.on("app/load", () => { const storedData = localStorage.getItem("h3_todo_list"); - const { todos, settings } = storedData ? JSON.parse(storedData) : {todos: [], settings: {}}; + const { todos, settings } = storedData + ? JSON.parse(storedData) + : { todos: [], settings: {} }; return { todos, settings }; }); h3.on("app/save", (state, data) => {@@ -37,13 +39,13 @@ text: data.text,
}); return { todos }; }); - h3.on("todos/remove", (state, data) => { - const todos = state.todos.filter(({ key }) => key !== data.key); + h3.on("todos/remove", (state, k) => { + const todos = state.todos.filter(({ key }) => key !== k); return { todos }; }); - h3.on("todos/toggle", (state, data) => { + h3.on("todos/toggle", (state, k) => { const todos = state.todos; - const todo = state.todos.find((t) => t.key === data.key); + const todo = state.todos.find(({ key }) => key === k); todo.done = !todo.done; return { todos }; });
@@ -61,6 +61,8 @@ };
const selectorRegex = /^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; +let $onrenderCallbacks = []; + // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) {@@ -68,7 +70,6 @@ this.type = undefined;
this.attributes = {}; this.data = {}; this.id = undefined; - this.$key = undefined; this.$html = undefined; this.$onrender = undefined; this.style = undefined;@@ -158,7 +159,6 @@ from(data) {
this.value = data.value; this.type = data.type; this.id = data.id; - this.$key = data.$key; this.$html = data.$html; this.$onrender = data.$onrender; this.style = data.style;@@ -176,7 +176,6 @@ }
processProperties(attrs) { this.id = this.id || attrs.id; - this.$key = attrs.$key; this.$html = attrs.$html; this.$onrender = attrs.$onrender; this.style = attrs.style;@@ -199,7 +198,6 @@ this.eventListeners[key.slice(2)] = attrs[key];
delete this.attributes[key]; }); delete this.attributes.value; - delete this.attributes.$key; delete this.attributes.$html; delete this.attributes.$onrender; delete this.attributes.id;@@ -301,7 +299,7 @@ // Children
this.children.forEach((c) => { const cnode = c.render(); node.appendChild(cnode); - c.$onrender && c.$onrender(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); }); if (this.$html) { node.innerHTML = this.$html;@@ -327,8 +325,6 @@ newvnode.$onrender && newvnode.$onrender(renderedNode);
oldvnode.from(newvnode); return; } - // $key - oldvnode.$key = newvnode.$key; // ID if (oldvnode.id !== newvnode.id) { node.id = newvnode.id || "";@@ -416,41 +412,67 @@ });
oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - const maxLength = Math.max( - oldvnode.children.length, - newvnode.children.length - ); let map = []; + let oldNodesFound = 0; + let newNodesFound = 0; + // First look for existing nodes for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - if (oldIndex >= newvnode.children.length) { - // not found in new node, remove from old - map.push(-3); - } else { - let found = -1; - for (let index = 0; index < oldvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - break; - } + let found = -1; + for (let index = 0; index < newvnode.children.length; index++) { + if ( + equal(oldvnode.children[oldIndex], newvnode.children[index]) && + !map.includes(index) + ) { + found = index; + newNodesFound++; + oldNodesFound++; + break; } - map.push(found); } + map.push(found); } - // other nodes are new, needs to be added - if (maxLength > oldvnode.children.length) { - map = map.concat( - [...Array(maxLength - oldvnode.children.length)].map(() => -2) - ); + if ( + newNodesFound === oldNodesFound && + newvnode.children.length === oldvnode.children.length + ) { + // something changed but everything else is the same + return map; + } + if (newNodesFound === newvnode.children.length) { + // All children in newvnode exist in oldvnode + // All nodes that are not found must be removed + for (let i = 0; i < map.length; i++) { + if (map[i] === -1) { + map[i] = -3; + } + } + } + if (oldNodesFound === oldvnode.children.length) { + // All children in oldvnode exist in newvnode + // Check where the missing newvnodes children need to be added + for ( + let newIndex = 0; + newIndex < newvnode.children.length; + newIndex++ + ) { + if (!map.includes(newIndex)) { + map.splice(newIndex, 0, -2); + } + } + } + // Check if nodes needs to be removed (if there are fewer children) + if (newvnode.children.length < oldvnode.children.length) { + for (let i = 0; i < map.length; i++) { + if (map[i] === -1 && !newvnode.children[i]) { + map[i] = -3; + } + } } return map; } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.length).keys()]; + let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; for (let i of childMap) {@@ -470,9 +492,9 @@ });
break; case -2: // add node - oldvnode.children.push(newvnode.children[count]); + oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); - node.appendChild(renderedNode); + node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); breakFor = true;@@ -642,6 +664,8 @@ const vnode = newRouteComponent(newRouteComponent.state);
const node = vnode.render(); this.element.appendChild(node); vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state); window.scrollTo(0, 0); this.store.dispatch("$redraw");
@@ -112,7 +112,6 @@
* Any attribute starting with *on* (e.g. onclick, onkeydown, etc.) will be treated as an event listener. * The `classList` attribute can be set to a list of classes to apply to the element (as an alternative to using the element selector shorthand). * The `data` attribute can be set to a simple object containing [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). -* The special `$key` attribute can be used to guarantee the uniqueness of two VNodes and it will not be translated into an HTML attribute. * The special `$html` attribute can be used to set the `innerHTML` property of the resulting HTML element. Use only if you know what you are doing! * The special `$onrender` attribute can be set to a function that will executed every time the VNode is rendered and added to the DOM.
@@ -61,6 +61,8 @@ };
const selectorRegex = /^([a-z][a-z0-9:_=-]*)(#[a-z0-9:_=-]+)?(\.[^ ]+)*$/i; +let $onrenderCallbacks = []; + // Virtual Node Implementation with HyperScript-like syntax class VNode { constructor(...args) {@@ -68,7 +70,6 @@ this.type = undefined;
this.attributes = {}; this.data = {}; this.id = undefined; - this.$key = undefined; this.$html = undefined; this.$onrender = undefined; this.style = undefined;@@ -158,7 +159,6 @@ from(data) {
this.value = data.value; this.type = data.type; this.id = data.id; - this.$key = data.$key; this.$html = data.$html; this.$onrender = data.$onrender; this.style = data.style;@@ -176,7 +176,6 @@ }
processProperties(attrs) { this.id = this.id || attrs.id; - this.$key = attrs.$key; this.$html = attrs.$html; this.$onrender = attrs.$onrender; this.style = attrs.style;@@ -199,7 +198,6 @@ this.eventListeners[key.slice(2)] = attrs[key];
delete this.attributes[key]; }); delete this.attributes.value; - delete this.attributes.$key; delete this.attributes.$html; delete this.attributes.$onrender; delete this.attributes.id;@@ -301,7 +299,7 @@ // Children
this.children.forEach((c) => { const cnode = c.render(); node.appendChild(cnode); - c.$onrender && c.$onrender(cnode); + c.$onrender && $onrenderCallbacks.push(() => c.$onrender(cnode)); }); if (this.$html) { node.innerHTML = this.$html;@@ -327,8 +325,6 @@ newvnode.$onrender && newvnode.$onrender(renderedNode);
oldvnode.from(newvnode); return; } - // $key - oldvnode.$key = newvnode.$key; // ID if (oldvnode.id !== newvnode.id) { node.id = newvnode.id || "";@@ -416,41 +412,67 @@ });
oldvnode.eventListeners = newvnode.eventListeners; } // Children - function mapChildren(oldvnode, newvnode) { - const maxLength = Math.max( - oldvnode.children.length, - newvnode.children.length - ); let map = []; + let oldNodesFound = 0; + let newNodesFound = 0; + // First look for existing nodes for (let oldIndex = 0; oldIndex < oldvnode.children.length; oldIndex++) { - if (oldIndex >= newvnode.children.length) { - // not found in new node, remove from old - map.push(-3); - } else { - let found = -1; - for (let index = 0; index < oldvnode.children.length; index++) { - if ( - equal(oldvnode.children[oldIndex], newvnode.children[index]) && - !map.includes(index) - ) { - found = index; - break; - } + let found = -1; + for (let index = 0; index < newvnode.children.length; index++) { + if ( + equal(oldvnode.children[oldIndex], newvnode.children[index]) && + !map.includes(index) + ) { + found = index; + newNodesFound++; + oldNodesFound++; + break; } - map.push(found); } + map.push(found); } - // other nodes are new, needs to be added - if (maxLength > oldvnode.children.length) { - map = map.concat( - [...Array(maxLength - oldvnode.children.length)].map(() => -2) - ); + if ( + newNodesFound === oldNodesFound && + newvnode.children.length === oldvnode.children.length + ) { + // something changed but everything else is the same + return map; + } + if (newNodesFound === newvnode.children.length) { + // All children in newvnode exist in oldvnode + // All nodes that are not found must be removed + for (let i = 0; i < map.length; i++) { + if (map[i] === -1) { + map[i] = -3; + } + } + } + if (oldNodesFound === oldvnode.children.length) { + // All children in oldvnode exist in newvnode + // Check where the missing newvnodes children need to be added + for ( + let newIndex = 0; + newIndex < newvnode.children.length; + newIndex++ + ) { + if (!map.includes(newIndex)) { + map.splice(newIndex, 0, -2); + } + } + } + // Check if nodes needs to be removed (if there are fewer children) + if (newvnode.children.length < oldvnode.children.length) { + for (let i = 0; i < map.length; i++) { + if (map[i] === -1 && !newvnode.children[i]) { + map[i] = -3; + } + } } return map; } let childMap = mapChildren(oldvnode, newvnode); - let resultMap = [...Array(childMap.length).keys()]; + let resultMap = [...Array(childMap.filter((i) => i !== -3).length).keys()]; while (!equal(childMap, resultMap)) { let count = -1; for (let i of childMap) {@@ -470,9 +492,9 @@ });
break; case -2: // add node - oldvnode.children.push(newvnode.children[count]); + oldvnode.children.splice(count, 0, newvnode.children[count]); const renderedNode = newvnode.children[count].render(); - node.appendChild(renderedNode); + node.insertBefore(renderedNode, node.childNodes[count]); newvnode.children[count].$onrender && newvnode.children[count].$onrender(renderedNode); breakFor = true;@@ -642,6 +664,8 @@ const vnode = newRouteComponent(newRouteComponent.state);
const node = vnode.render(); this.element.appendChild(node); vnode.$onrender && vnode.$onrender(node); + $onrenderCallbacks.forEach((cbk) => cbk()); + $onrenderCallbacks = []; this.setRedraw(vnode, newRouteComponent.state); window.scrollTo(0, 0); this.store.dispatch("$redraw");