From 04a6ea7322d086d1575e832a051ca4457d69e829 Mon Sep 17 00:00:00 2001 From: Lea Verou Date: Wed, 17 Jan 2018 22:01:48 +0200 Subject: [PATCH] Re-use objects created by getData(): 50% speedup in expression evaluation --- src/collection.js | 30 +++++++++++++++++++----------- src/group.js | 41 ++++++++++++++++++++++++----------------- src/mavo.js | 3 ++- src/node.js | 31 ++++++++++++++++++++----------- src/primitive.js | 9 ++++++++- src/util.js | 13 +++++++++++++ 6 files changed, 86 insertions(+), 41 deletions(-) diff --git a/src/collection.js b/src/collection.js index 5c4fa1c7..26945bb6 100644 --- a/src/collection.js +++ b/src/collection.js @@ -68,22 +68,29 @@ var _ = Mavo.Collection = $.Class({ var env = { context: this, options: o, - data: [] + data: this.liveData }; - this.children.forEach(item => { + if (env.options.live) { + this.proxyCache = {}; + } + + for (var i = 0, j = 0; item = this.children[i]; i++) { if (!item.deleted || env.options.live) { var itemData = item.getData(env.options); if (env.options.live || Mavo.value(itemData) !== null) { - env.data.push(itemData); + env.data[j] = itemData; + j++; } } - }); + } + + env.data.length = j; if (!this.mutable) { // If immutable, drop nulls - env.data = env.data.filter(item => Mavo.value(item) !== null); + Mavo.filter(env.data, item => Mavo.value(item) !== null); if (env.options.live && env.data.length === 1) { // If immutable with only 1 item, return the item @@ -96,18 +103,13 @@ var _ = Mavo.Collection = $.Class({ } } - if (env.options.live && Array.isArray(env.data)) { - env.data[Mavo.toNode] = this; - env.data = this.relativizeData(env.data); - } - if (!env.options.live) { env.data = Mavo.subset(this.data, this.inPath, env.data); } Mavo.hooks.run("node-getdata-end", env); - return env.data; + return (env.options.live? env.data[Mavo.toProxy] : env.data) || env.data; }, // Create item but don't insert it anywhere @@ -437,6 +439,8 @@ var _ = Mavo.Collection = $.Class({ } } } + + this.createLiveData(data|| []); }, find: function(property, o = {}) { @@ -619,6 +623,10 @@ var _ = Mavo.Collection = $.Class({ }); return button; + }, + + liveData: function() { + return this.createLiveData([]); } }, diff --git a/src/group.js b/src/group.js index 56b86236..1fa00bf0 100644 --- a/src/group.js +++ b/src/group.js @@ -44,6 +44,8 @@ var _ = Mavo.Group = $.Class({ } }); + this.childrenNames = Object.keys(this.children); + var vocabElement = (this.isRoot? this.element.closest("[vocab]") : null) || this.element; this.vocab = vocabElement.getAttribute("vocab"); @@ -72,11 +74,9 @@ var _ = Mavo.Group = $.Class({ return env.data; } - env.data = (this.data? Mavo.clone(Mavo.subset(this.data, this.inPath)) : {}) || {}; - - var properties = Object.keys(this.children); + env.data = this.liveData; - if (properties.length == 1 && properties[0] == this.property) { + if (this.childrenNames.length == 1 && this.childrenNames[0] == this.property) { // {foo: {foo: 5}} should become {foo: 5} var options = $.extend($.extend({}, env.options), {forceObjects: true}); env.data = this.children[this.property].getData(options); @@ -87,22 +87,25 @@ var _ = Mavo.Group = $.Class({ if (obj.saved || env.options.live) { var data = obj.getData(env.options); + } - if (data === null && !env.options.live) { - delete env.data[obj.property]; - } - else { - env.data[obj.property] = data; - } + if (env.options.live || obj.saved && Mavo.value(data) !== null) { + env.data[obj.property] = data; + } + else { + delete env.data[obj.property]; } } } - if (!env.options.live) { // Stored data again + if (env.options.live) { + this.proxyCache = {}; + } + else { // Stored data again // If storing, use the rendered data too env.data = Mavo.subset(this.data, this.inPath, env.data); - if (!properties.length && !this.isRoot) { + if (!this.childrenNames.length && !this.isRoot) { // Avoid {} in the data env.data = null; } @@ -117,14 +120,10 @@ var _ = Mavo.Group = $.Class({ } } } - else if (env.data) { - env.data[Mavo.toNode] = this; - env.data = this.relativizeData(env.data); - } Mavo.hooks.run("node-getdata-end", env); - return env.data; + return (env.options.live? env.data[Mavo.toProxy] : env.data) || env.data; }, /** @@ -251,6 +250,14 @@ var _ = Mavo.Group = $.Class({ } } } + + this.createLiveData(data); + }, + + lazy: { + liveData: function() { + return this.createLiveData(); + } }, static: { diff --git a/src/mavo.js b/src/mavo.js index a6dfe115..7e9984a3 100644 --- a/src/mavo.js +++ b/src/mavo.js @@ -683,7 +683,8 @@ var _ = self.Mavo = $.Class({ lazy: { locale: () => document.documentElement.lang || "en-GB", - toNode: () => Symbol("toNode") + toNode: () => Symbol("toNode"), + toProxy: () => Symbol("toProxy") } } }); diff --git a/src/node.js b/src/node.js index c0a84c48..510560d4 100644 --- a/src/node.js +++ b/src/node.js @@ -363,8 +363,6 @@ var _ = Mavo.Node = $.Class({ }, relativizeData: self.Proxy? function(data, options = {live: true}) { - var cache = {}; - return new Proxy(data, { get: (data, property, proxy) => { if (property in data) { @@ -372,13 +370,13 @@ var _ = Mavo.Node = $.Class({ } // Checking if property is in proxy might add it to the cache - if (property in proxy && property in cache) { - return cache[property]; + if (property in proxy && property in this.proxyCache) { + return this.proxyCache[property]; } }, has: (data, property) => { - if (property in data || property in cache) { + if (property in data || property in this.proxyCache) { return true; } @@ -387,16 +385,16 @@ var _ = Mavo.Node = $.Class({ // Special values switch (property) { case "$index": - cache[property] = this.index || 0; + this.proxyCache[property] = this.index || 0; return true; // if index is 0 it's falsy and has would return false! case "$next": case "$previous": if (this.closestCollection) { - cache[property] = this.closestCollection.getData(options)[this.index + (property == "$next"? 1 : -1)]; + this.proxyCache[property] = this.closestCollection.getData(options)[this.index + (property == "$next"? 1 : -1)]; return true; } - cache[property] = null; + this.proxyCache[property] = null; return false; } @@ -412,14 +410,14 @@ var _ = Mavo.Node = $.Class({ ret = ret.getData(options); } - cache[property] = ret; + this.proxyCache[property] = ret; return true; } // Does it reference another Mavo? - if (property in Mavo.all && Mavo.all[property].root) { - return cache[property] = Mavo.all[property].root.getData(options); + if (property in Mavo.all && isNaN(property) && Mavo.all[property].root) { + return this.proxyCache[property] = Mavo.all[property].root.getData(options); } return false; @@ -432,6 +430,13 @@ var _ = Mavo.Node = $.Class({ }); } : data => data, + createLiveData: function(obj = {}) { + this.liveData = obj; + this.liveData[Mavo.toNode] = this; + this.liveData[Mavo.toProxy] = this.relativizeData(this.liveData); + return this.liveData; + }, + pathFrom: function(node) { var path = this.path; var nodePath = node.path; @@ -537,6 +542,10 @@ var _ = Mavo.Node = $.Class({ } return ret; + }, + + proxyCache: function() { + return {}; } }, diff --git a/src/primitive.js b/src/primitive.js index 63978b7a..c3be3910 100644 --- a/src/primitive.js +++ b/src/primitive.js @@ -219,12 +219,19 @@ var _ = Mavo.Primitive = $.Class({ [Mavo.toNode]: this }); + env.data[Mavo.toProxy] = this.relativizeData(env.data); + if (this.collection) { // Turn primitive collection items into objects, so we can have $index etc, and their property // name etc resolve relative to them, not their parent group env.data[this.property] = env.data; - env.data = this.relativizeData(env.data); } + + Mavo.hooks.run("node-getdata-end", env); + + this.proxyCache = {}; + + return env.data[Mavo.toProxy]; } } else if (this.inPath.length) { diff --git a/src/util.js b/src/util.js index 6de8c612..23c69246 100644 --- a/src/util.js +++ b/src/util.js @@ -106,6 +106,8 @@ var _ = $.extend(Mavo, { return arr === undefined? [] : Array.isArray(arr)? arr : [arr]; }, + // Delete an element from an array + // @param all {Boolean} Delete more than one? delete: (arr, element, all) => { do { var index = arr && arr.indexOf(element); @@ -136,6 +138,17 @@ var _ = $.extend(Mavo, { return new Set([...(set1 || []), ...(set2 || [])]); }, + // Filter an array in place + // TODO add index to callback + filter: (arr, callback) => { + for (var i=0; i