diff --git a/README.md b/README.md index 424fcb2b..5b691d56 100644 --- a/README.md +++ b/README.md @@ -566,6 +566,14 @@ Also see [examples/usage-tweenState.js](examples/usage-tweenState.js) ## Changelog +**1.1.10** +- better rendering performance: debounce the `render()` function: + - `setState()` can be called 1000s of times a second + - `setState()` also calls the render() function + - `render()` will only update the DOM at 60fps +- see https://gomakethings.com/debouncing-your-javascript-events/ +- updated README + **1.1.9** - new feature: "middleware" - define an array of functions as `myComponent.middleware = [ someFunc, otherFunc ]` diff --git a/dist/component.min.js b/dist/component.min.js index f5ac82da..fca59fa8 100644 --- a/dist/component.min.js +++ b/dist/component.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Component=e()}(this,(function(){"use strict";var t=(t,e)=>{const i={cfg:{orig:t},replace(t,e=t){const i=document.createElement("template");i.innerHTML=e;const s=i.content.firstChild.nextElementSibling;s.nodeName===t.nodeName?this.iterate(t,s):t.parentElement.replaceChild(s,t)},iterate(e,i,s){(e||i)&&(this.checkAdditions(e,i,s),e&&i&&e.nodeName!==i.nodeName?this.checkNodeName(e,i):e&&i&&e.nodeName===i.nodeName&&(this.checkTextContent(e,i),3!==e.nodeType&&8!==t.nodeType&&this.checkAttributes(e,i))),e&&i&&(e.childNodes&&i.childNodes?this.cfg.lengthDifferentiator=[...t.childNodes,...i.childNodes]:this.cfg.lengthDifferentiator=null,Array.apply(null,this.cfg.lengthDifferentiator).forEach((t,s)=>{this.cfg.lengthDifferentiator&&this.iterate(e.childNodes[s],i.childNodes[s],e,i)}))},checkNodeName(t,e){const i=e.cloneNode(!0);t.parentElement.replaceChild(i,t)},checkAttributes(t,e){const i=t.attributes||[],s=Object.keys(i).map(t=>i[t]),n=e.attributes||[],h=Object.keys(n).map(t=>n[t]);s.forEach(i=>null!==e.getAttribute(i.name)?t.setAttribute(i.name,e.getAttribute(i.name)):t.removeAttribute(i.name)),h.forEach(i=>t.getAttribute(i.name)!==e.getAttribute(i.name)&&t.setAttribute(i.name,e.getAttribute(i.name)))},checkTextContent(t,e){t.nodeValue!==e.nodeValue&&(t.textContent=e.textContent)},checkAdditions(t,e,i=this.cfg.orig){if(e&&void 0===t){const t=e.cloneNode(!0);3!==i.nodeType&&8!==i.nodeType&&i.appendChild(t)}else t&&void 0===e&&t.parentElement.removeChild(t)}};Object.create(i).replace(t,e)};return function e(i){this.reactive=!0,this.debug=!1,this.scopedCss=!0;var s=this;return this.isNode="undefined"!=typeof process&&null!==process&&null!==process.versions&&null!==process.versions.node,this.state=i,this.view=t=>t,this.middleware=[],this.uid=Math.random().toString(36).split(".")[1],this.isNode||(this.css=document.createElement("style"),document.head.appendChild(this.css)),this.log=[{id:0,state:i,action:"init"}],this.actions=t=>(Object.keys(t).forEach(e=>{if(void 0!==this[e])return!1;this[e]=i=>(this.action=e,t[e](i))}),this),this.freeze=t=>(Object.isFrozen(t)||(Object.keys(t).forEach(e=>this.freeze(t[e])),Object.freeze(t)),t),this.eq=(t,e)=>{const i=Object.keys,s=typeof t;return t&&e&&"object"===s&&s===typeof e?i(t).length===i(e).length&&i(t).every(i=>this.eq(t[i],e[i])):String(t)===String(e)},this.setState=t=>{if(this.prev=this.state,this.state={...this.state,...t},!this.eq(this.state,this.prev))return this.reactive&&this.render(this.container),this.debug&&this.i===this.log.length&&this.log.push({id:this.log.length,state:this.prev,action:this.action||"setState"}),this.i=this.log.length,this.freeze(this.state),void 0!==e.emitter&&this.action&&e.emitter.emit(""+this.action,this.state),this.middleware.forEach(t=>t({...this.state})),this.action=void 0,this},this.tweenState=(t,i)=>(void 0!==e.tweenState?e.tweenState(s,t,i):this.setState(t),this),this.on=(t,i)=>(void 0!==e.emitter&&e.emitter.on(t,i),this),this.once=(t,i)=>(void 0!==e.emitter&&e.emitter.on(t,s=>{i(s),e.emitter.off(t,i)}),this),this.off=(t,i)=>(void 0!==e.emitter&&e.emitter.off(t,i),this),this.go=function(t,e){var i;return i="f"===e?this.i+t:this.i-t,this.log[i]&&this.setState(this.log[i].state),this.i=i,this},this.rw=function(t){return t?(this.go(t,"b"),this):(this.log[0]&&this.setState(this.log[0].state),this.i=0,!0)},this.ff=function(t){return t?(this.go(t,"f"),this):(this.log[this.log.length-1]&&this.setState(this.log[this.log.length-1].state),this.i=this.log.length-1,!0)},this.setCss=function(){if(!this.isNode&&this.css&&"function"==typeof this.style){var t=this.style(this.state);if(this.scopedCss){var e=this.container.id?this.container.id:this.container.className;e=e||this.uid;var i=this.container.id?"#":".",s=new RegExp(e+" ,\\s*\\.","gm"),n=new RegExp(e+" ,\\s*#","gm"),h=new RegExp(e+" ,\\s*([a-z\\.#])","gmi");t=t.replace(/}/g,"}\n").replace(/\;\s*\n/g,";").replace(/{\s*\n/g,"{ ").replace(/^\s+|\s+$/gm,"\n").replace(/(^[\.#\w][\w\-]*|\s*,[\.#\w][\w\-]*)/gm,i+e+" $1").replace(s,", "+i+e+" ").replace(n,", "+i+e+" #").replace(h,", "+i+e+" $1").replace(/\n/g,"").replace(/\s\s+/g," ")}var o=t.replace(/\n/g,"").replace(/\s\s+/g," ");this.css.innerHTML!==o&&(this.css.innerHTML=o)}},this.toString=function(){var t,e="",i=this.view(this.state);if("function"==typeof this.style&&(t=this.style(this.state).replace(/^ {4}/gm,"")),"string"==typeof i)try{e=JSON.parse(i),e=JSON.stringify(e,null,2)}catch(s){t&&(e=``),e+=(""+i).replace(/^ {4}/gm,"")}else("object"==typeof i||Array.isArray(i))&&(e=JSON.stringify(i,null,2));return e},this.render=function(e){var i="function"==typeof this.view?this.view(this.state):null;return this.isNode?this.toString():(this.container=e,requestAnimationFrame(()=>{if("string"==typeof e&&(e=document.querySelector(""+e)),this.container=e,this.css&&this.style&&this.setCss(),this.container&&i)try{t(this.container.firstChild,i)}catch(t){this.container.innerHTML=i}}),this.container)},this}})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Component=e()}(this,(function(){"use strict";var t=(t,e)=>{const i={cfg:{orig:t},replace(t,e=t){const i=document.createElement("template");i.innerHTML=e;const s=i.content.firstChild.nextElementSibling;s.nodeName===t.nodeName?this.iterate(t,s):t.parentElement.replaceChild(s,t)},iterate(e,i,s){(e||i)&&(this.checkAdditions(e,i,s),e&&i&&e.nodeName!==i.nodeName?this.checkNodeName(e,i):e&&i&&e.nodeName===i.nodeName&&(this.checkTextContent(e,i),3!==e.nodeType&&8!==t.nodeType&&this.checkAttributes(e,i))),e&&i&&(e.childNodes&&i.childNodes?this.cfg.lengthDifferentiator=[...t.childNodes,...i.childNodes]:this.cfg.lengthDifferentiator=null,Array.apply(null,this.cfg.lengthDifferentiator).forEach((t,s)=>{this.cfg.lengthDifferentiator&&this.iterate(e.childNodes[s],i.childNodes[s],e,i)}))},checkNodeName(t,e){const i=e.cloneNode(!0);t.parentElement.replaceChild(i,t)},checkAttributes(t,e){const i=t.attributes||[],s=Object.keys(i).map(t=>i[t]),n=e.attributes||[],h=Object.keys(n).map(t=>n[t]);s.forEach(i=>null!==e.getAttribute(i.name)?t.setAttribute(i.name,e.getAttribute(i.name)):t.removeAttribute(i.name)),h.forEach(i=>t.getAttribute(i.name)!==e.getAttribute(i.name)&&t.setAttribute(i.name,e.getAttribute(i.name)))},checkTextContent(t,e){t.nodeValue!==e.nodeValue&&(t.textContent=e.textContent)},checkAdditions(t,e,i=this.cfg.orig){if(e&&void 0===t){const t=e.cloneNode(!0);3!==i.nodeType&&8!==i.nodeType&&i.appendChild(t)}else t&&void 0===e&&t.parentElement.removeChild(t)}};Object.create(i).replace(t,e)};return function e(i){this.reactive=!0,this.debug=!1,this.scopedCss=!0;var s,n=this;return this.isNode="undefined"!=typeof process&&null!==process&&null!==process.versions&&null!==process.versions.node,this.state=i,this.view=t=>t,this.middleware=[],this.uid=Math.random().toString(36).split(".")[1],this.isNode||(this.css=document.createElement("style"),document.head.appendChild(this.css)),this.log=[{id:0,state:i,action:"init"}],this.actions=t=>(Object.keys(t).forEach(e=>{if(void 0!==this[e])return!1;this[e]=i=>(this.action=e,t[e](i))}),this),this.freeze=t=>(Object.isFrozen(t)||(Object.keys(t).forEach(e=>this.freeze(t[e])),Object.freeze(t)),t),this.eq=(t,e)=>{const i=Object.keys,s=typeof t;return t&&e&&"object"===s&&s===typeof e?i(t).length===i(e).length&&i(t).every(i=>this.eq(t[i],e[i])):String(t)===String(e)},this.setState=t=>{if(this.prev=this.state,this.state={...this.state,...t},!this.eq(this.state,this.prev))return this.reactive&&this.render(this.container),this.debug&&this.i===this.log.length&&this.log.push({id:this.log.length,state:this.prev,action:this.action||"setState"}),this.i=this.log.length,this.freeze(this.state),void 0!==e.emitter&&this.action&&e.emitter.emit(""+this.action,this.state),this.middleware.forEach(t=>t({...this.state})),this.action=void 0,this},this.tweenState=(t,i)=>(void 0!==e.tweenState?e.tweenState(n,t,i):this.setState(t),this),this.on=(t,i)=>(void 0!==e.emitter&&e.emitter.on(t,i),this),this.once=(t,i)=>(void 0!==e.emitter&&e.emitter.on(t,s=>{i(s),e.emitter.off(t,i)}),this),this.off=(t,i)=>(void 0!==e.emitter&&e.emitter.off(t,i),this),this.go=function(t,e){var i;return i="f"===e?this.i+t:this.i-t,this.log[i]&&this.setState(this.log[i].state),this.i=i,this},this.rw=function(t){return t?(this.go(t,"b"),this):(this.log[0]&&this.setState(this.log[0].state),this.i=0,!0)},this.ff=function(t){return t?(this.go(t,"f"),this):(this.log[this.log.length-1]&&this.setState(this.log[this.log.length-1].state),this.i=this.log.length-1,!0)},this.setCss=function(){if(!this.isNode&&this.css&&"function"==typeof this.style){var t=this.style(this.state);if(this.scopedCss){var e=this.container.id?this.container.id:this.container.className;e=e||this.uid;var i=this.container.id?"#":".",s=new RegExp(e+" ,\\s*\\.","gm"),n=new RegExp(e+" ,\\s*#","gm"),h=new RegExp(e+" ,\\s*([a-z\\.#])","gmi");t=t.replace(/}/g,"}\n").replace(/\;\s*\n/g,";").replace(/{\s*\n/g,"{ ").replace(/^\s+|\s+$/gm,"\n").replace(/(^[\.#\w][\w\-]*|\s*,[\.#\w][\w\-]*)/gm,i+e+" $1").replace(s,", "+i+e+" ").replace(n,", "+i+e+" #").replace(h,", "+i+e+" $1").replace(/\n/g,"").replace(/\s\s+/g," ")}var o=t.replace(/\n/g,"").replace(/\s\s+/g," ");this.css.innerHTML!==o&&(this.css.innerHTML=o)}},this.toString=function(){var t,e="",i=this.view(this.state);if("function"==typeof this.style&&(t=this.style(this.state).replace(/^ {4}/gm,"")),"string"==typeof i)try{e=JSON.parse(i),e=JSON.stringify(e,null,2)}catch(s){t&&(e=``),e+=(""+i).replace(/^ {4}/gm,"")}else("object"==typeof i||Array.isArray(i))&&(e=JSON.stringify(i,null,2));return e},this.render=function(e){var i="function"==typeof this.view?this.view(this.state):null;return this.isNode?this.toString():(this.container=e,s&&cancelAnimationFrame(s),s=requestAnimationFrame(()=>{if("string"==typeof e&&(e=document.querySelector(""+e)),this.container=e,this.css&&this.style&&this.setCss(),this.container&&i)try{t(this.container.firstChild,i)}catch(t){this.container.innerHTML=i}}),this.container)},this}})); diff --git a/examples/usage-in-browser.html b/examples/usage-in-browser.html index e4887a8c..c1a06f6f 100644 --- a/examples/usage-in-browser.html +++ b/examples/usage-in-browser.html @@ -163,5 +163,13 @@ console.log('App.log') console.log(App.log) + // Test debouncing of render(): + // - call setState 10,000 times, fast as possible + // - setState calls render() each time + // - render() should only try to access/update DOM at 60fps + for (var i = 0; i < 10000; i++) { + App.plus(1) + } + diff --git a/package-lock.json b/package-lock.json index 2f36d42d..d1360c72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@scottjarvis/component", - "version": "1.1.8", + "version": "1.1.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 7dddf3a7..ef214295 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@scottjarvis/component", - "version": "1.1.9", + "version": "1.1.10", "description": "Simple component library with state management, DOM diffing, tweening, server-side rendering and more.", "author": "sc0ttj", "license": "MIT", diff --git a/src/component.js b/src/component.js index 11bb810a..7159580a 100644 --- a/src/component.js +++ b/src/component.js @@ -6,6 +6,7 @@ // from https://codepen.io/tevko/pen/LzXjKE?editors=0010 var domDiff = (target, source) => { + //console.log("updating DOM!") const worker = { cfg: { orig: target @@ -116,6 +117,9 @@ function Component(state) { var self = this + // for debouncing render() calls + var timeout + this.isNode = typeof process !== "undefined" && process !== null && @@ -404,7 +408,13 @@ function Component(state) { } else { this.container = el // only interact with the DOM once every animation frame (usually 60fps) - requestAnimationFrame(() => { + + // If there's a timer, cancel it + if (timeout) cancelAnimationFrame(timeout) + + // Setup the new requestAnimationFrame() + timeout = requestAnimationFrame(() => { + //console.log("debounced") // get the container element if needed if (typeof el === "string") el = document.querySelector(`${el}`) this.container = el