From 2ffd2fb7e4123de483b3b720bcb4fa9dd8e4062e Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sun, 4 Dec 2016 01:53:39 -0500 Subject: [PATCH] throttle m.route redraws --- api/mount.js | 27 ++------------ api/redraw.js | 23 +++++++++++- api/router.js | 4 +- api/tests/test-mount.js | 6 ++- api/tests/test-redraw.js | 27 +++++++++++--- api/tests/test-router.js | 24 ++++++++++++ mithril.js | 74 ++++++++++++++++++------------------- mithril.min.js | 80 ++++++++++++++++++++-------------------- router/router.js | 15 ++++---- 9 files changed, 161 insertions(+), 119 deletions(-) diff --git a/api/mount.js b/api/mount.js index 2afadc57..0cebfcf6 100644 --- a/api/mount.js +++ b/api/mount.js @@ -3,27 +3,6 @@ var Vnode = require("../render/vnode") module.exports = function(redrawService) { - function throttle(callback) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function() { - var now = Date.now() - if (last === 0 || now - last >= time) { - last = now - callback() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback() - last = Date.now() - }, time - (now - last)) - } - } - } - return function(root, component) { if (component === null) { redrawService.render(root, []) @@ -33,10 +12,10 @@ module.exports = function(redrawService) { if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") - var run = throttle(function() { + var run = function() { redrawService.render(root, Vnode(component)) - }) + } redrawService.subscribe(root, run) - run() + redrawService.redraw() } } diff --git a/api/redraw.js b/api/redraw.js index d2d20c52..3f98b061 100644 --- a/api/redraw.js +++ b/api/redraw.js @@ -2,6 +2,27 @@ var coreRenderer = require("../render/render") +function throttle(callback) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var now = Date.now() + if (last === 0 || now - last >= time) { + last = now + callback() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback() + last = Date.now() + }, time - (now - last)) + } + } +} + module.exports = function($window) { var renderService = coreRenderer($window) renderService.setEventCallback(function(e) { @@ -11,7 +32,7 @@ module.exports = function($window) { var callbacks = [] function subscribe(key, callback) { unsubscribe(key) - callbacks.push(key, callback) + callbacks.push(key, throttle(callback)) } function unsubscribe(key) { var index = callbacks.indexOf(key) diff --git a/api/router.js b/api/router.js index 553a6751..c5306174 100644 --- a/api/router.js +++ b/api/router.js @@ -17,11 +17,11 @@ module.exports = function($window, redrawService) { current.resolve = null redrawService.render(root, current.render(Vnode(component, undefined, params))) } - var run = routeService.defineRoutes(routes, function(component, params, path, route, isRouteChange) { + var run = routeService.defineRoutes(routes, function(component, params, path, route, isAction) { if (component.view) render({}, component, params, path) else { if (component.onmatch) { - if (isRouteChange === false && current.path === path || current.resolve != null) render(current, current.component, params) + if (isAction === false && current.path === path || current.resolve != null) render(current, current.component, params) else { current.resolve = function(resolved) { render(component, resolved, params, path) diff --git a/api/tests/test-mount.js b/api/tests/test-mount.js index fda30a7f..210a0627 100644 --- a/api/tests/test-mount.js +++ b/api/tests/test-mount.js @@ -221,14 +221,16 @@ o.spec("mount", function() { mount(root, {view: function() {i++}}) var before = i + redrawService.redraw() + redrawService.redraw() redrawService.redraw() redrawService.redraw() var after = i setTimeout(function(){ - o(before).equals(1) - o(after).equals(1) + o(before).equals(1) // mounts synchronously + o(after).equals(1) // throttles rest o(i).equals(2) done() },40) diff --git a/api/tests/test-redraw.js b/api/tests/test-redraw.js index 5f002e5f..f13c2d3f 100644 --- a/api/tests/test-redraw.js +++ b/api/tests/test-redraw.js @@ -17,11 +17,13 @@ o.spec("redrawService", function() { redrawService.redraw() }) - o("should run a single renderer entry", function() { + o("should run a single renderer entry", function(done) { var spy = o.spy() redrawService.subscribe(root, spy) + o(spy.callCount).equals(0) + redrawService.redraw() o(spy.callCount).equals(1) @@ -30,10 +32,15 @@ o.spec("redrawService", function() { redrawService.redraw() redrawService.redraw() - o(spy.callCount).equals(4) + o(spy.callCount).equals(1) + setTimeout(function() { + o(spy.callCount).equals(2) + + done() + }, 20) }) - o("should run all renderer entries", function() { + o("should run all renderer entries", function(done) { var el1 = $document.createElement("div") var el2 = $document.createElement("div") var el3 = $document.createElement("div") @@ -53,9 +60,17 @@ o.spec("redrawService", function() { redrawService.redraw() - o(spy1.callCount).equals(2) - o(spy2.callCount).equals(2) - o(spy3.callCount).equals(2) + o(spy1.callCount).equals(1) + o(spy2.callCount).equals(1) + o(spy3.callCount).equals(1) + + setTimeout(function() { + o(spy1.callCount).equals(2) + o(spy2.callCount).equals(2) + o(spy3.callCount).equals(2) + + done() + }, 20) }) o("should stop running after unsubscribe", function() { diff --git a/api/tests/test-router.js b/api/tests/test-router.js index e734d676..5b7ddf3e 100644 --- a/api/tests/test-router.js +++ b/api/tests/test-router.js @@ -608,6 +608,30 @@ o.spec("route", function() { done() }, 30) }) + + o("throttles", function(done, timeout) { + timeout(200) + + var i = 0 + $window.location.href = prefix + "/" + route(root, "/", { + "/": {view: function(v) {i++}} + }) + var before = i + + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + redrawService.redraw() + var after = i + + setTimeout(function(){ + o(before).equals(1) // routes synchronously + o(after).equals(2) // redraws synchronously + o(i).equals(3) // throttles rest + done() + },40) + }) }) }) }) diff --git a/mithril.js b/mithril.js index 312b4cc0..1493aa46 100644 --- a/mithril.js +++ b/mithril.js @@ -881,7 +881,7 @@ var coreRenderer = function($window) { if (!dom) throw new Error("Ensure the DOM element being passed to m.route/m.mount/m.render is not undefined.") var hooks = [] var active = $doc.activeElement - // First time rendering into a node clears it out + // First time0 rendering into a node clears it out if (dom.vnodes == null) dom.textContent = "" if (!(vnodes instanceof Array)) vnodes = [vnodes] updateNodes(dom, dom.vnodes, Vnode.normalizeChildren(vnodes), hooks, null, undefined) @@ -891,6 +891,26 @@ var coreRenderer = function($window) { } return {render: render, setEventCallback: setEventCallback} } +function throttle(callback) { + //60fps translates to 16.6ms, round it down since setTimeout requires int + var time = 16 + var last = 0, pending = null + var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout + return function() { + var now = Date.now() + if (last === 0 || now - last >= time) { + last = now + callback() + } + else if (pending === null) { + pending = timeout(function() { + pending = null + callback() + last = Date.now() + }, time - (now - last)) + } + } +} var _11 = function($window) { var renderService = coreRenderer($window) renderService.setEventCallback(function(e) { @@ -900,7 +920,7 @@ var _11 = function($window) { var callbacks = [] function subscribe(key1, callback) { unsubscribe(key1) - callbacks.push(key1, callback) + callbacks.push(key1, throttle(callback)) } function unsubscribe(key1) { var index = callbacks.indexOf(key1) @@ -916,27 +936,6 @@ var _11 = function($window) { var redrawService = _11(window) requestService.setCompletionCallback(redrawService.redraw) var _16 = function(redrawService0) { - function throttle(callback0) { - //60fps translates to 16.6ms, round it down since setTimeout requires int - var time = 16 - var last = 0, pending = null - var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout - return function() { - var now = Date.now() - if (last === 0 || now - last >= time) { - last = now - callback0() - } - else if (pending === null) { - pending = timeout(function() { - pending = null - callback0() - last = Date.now() - }, time - (now - last)) - } - } - } - return function(root, component) { if (component === null) { redrawService0.render(root, []) @@ -946,11 +945,11 @@ var _16 = function(redrawService0) { if (component.view == null) throw new Error("m.mount(element, component) expects a component, not a vnode") - var run0 = throttle(function() { + var run0 = function() { redrawService0.render(root, Vnode(component)) - }) + } redrawService0.subscribe(root, run0) - run0() + redrawService0.redraw() } } m.mount = _16(redrawService) @@ -1049,11 +1048,7 @@ var coreRouter = function($window) { else $window.location.href = prefix1 + path } function defineRoutes(routes, resolve, reject) { - if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) - else if (prefix1.charAt(0) === "#") $window.onhashchange = resolveRoute - resolveRoute(true) - - function resolveRoute(isRouteChange) { + function resolveRoute(isAction) { var path = getPath() var params = {} var pathname = parsePath(path, params, params) @@ -1067,14 +1062,19 @@ var coreRouter = function($window) { for (var i = 0; i < keys.length; i++) { params[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]) } - resolve(routes[route0], params, path, route0, Boolean(isRouteChange)) + resolve(routes[route0], params, path, route0, Boolean(isAction)) }) return } } reject(path, params) } - return function() {resolveRoute(false)} + + if (supportsPushState) $window.onpopstate = debounceAsync(resolveRoute) + else if (prefix1.charAt(0) === "#") $window.onhashchange = resolveRoute + resolveRoute(true) + + return resolveRoute } function link(vnode2) { vnode2.dom.setAttribute("href", prefix1 + vnode2.attrs.href) @@ -1103,11 +1103,11 @@ var _20 = function($window, redrawService0) { current.resolve = null redrawService0.render(root, current.render(Vnode(component, undefined, params))) } - var run1 = routeService.defineRoutes(routes, function(component, params, path, route, isRouteChange) { + var run1 = routeService.defineRoutes(routes, function(component, params, path, route, isAction) { if (component.view) render1({}, component, params, path) else { if (component.onmatch) { - if (isRouteChange === false && current.path === path || current.resolve != null) render1(current, current.component, params) + if (isAction === false && current.path === path || current.resolve != null) render1(current, current.component, params) else { current.resolve = function(resolved) { render1(component, resolved, params, path) @@ -1131,9 +1131,9 @@ var _20 = function($window, redrawService0) { return route } m.route = _20(window, redrawService) -m.withAttr = function(attrName, callback1, context) { +m.withAttr = function(attrName, callback0, context) { return function(e) { - return callback1.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) + return callback0.call(context || this, attrName in e.currentTarget ? e.currentTarget[attrName] : e.currentTarget.getAttribute(attrName)) } } var _27 = coreRenderer(window) diff --git a/mithril.min.js b/mithril.min.js index f0a924aa..a218474f 100644 --- a/mithril.min.js +++ b/mithril.min.js @@ -1,41 +1,41 @@ -new function(){function t(a,f,g,c,d,h){return{tag:a,key:f,attrs:g,children:c,text:d,dom:h,domSize:void 0,state:{},events:void 0,instance:void 0,skip:!1}}function A(a){if(null==a||"string"!==typeof a&&null==a.view)throw Error("The selector must be either a string or a component.");if("string"===typeof a&&void 0===I[a]){for(var f,g,c=[],d={};f=N.exec(a);){var h=f[1],m=f[2];""===h&&""!==m?g=m:"#"===h?d.id=m:"."===h?c.push(m):"["===f[3][0]&&((h=f[6])&&(h=h.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")), -"class"===f[4]?c.push(h):d[f[4]]=h||!0)}0b.indexOf("?")?"?":"&";b+=d+c}return b}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b);}}function z(b){return b.responseText}function n(b,a){if("function"===typeof b)if(a instanceof Array)for(var c=0;cg.status||304===g.status)c(n(b.type, -a));else{var d=Error(g.responseText),h;for(h in a)d[h]=a[h];f(d)}}catch(G){f(G)}};k&&null!=b.data?g.send(b.data):g.send()});return!0===b.background?r:v(r)},jsonp:function(b,k){var m=g();b=c(b,k);var r=new f(function(c,f){var g=b.callbackName||"_mithril_"+Math.round(1E16*Math.random())+"_"+t++,k=a.document.createElement("script");a[g]=function(d){k.parentNode.removeChild(k);c(n(b.type,d));delete a[g]};k.onerror=function(){k.parentNode.removeChild(k);f(Error("JSONP request failed"));delete a[g]};null== -b.data&&(b.data={});b.url=d(b.url,b.data);b.data[b.callbackKey||"callback"]=g;k.src=h(b.url,b.data);a.document.documentElement.appendChild(k)});return!0===b.background?r:m(r)},setCompletionCallback:function(a){k=a}}}(window,"undefined"!==typeof Promise?Promise:u),M=function(a){function f(e,l,a,b,c,d,f){for(;a=w&&v>=y;){var x=l[w],q=a[y];if(x!==q||p)if(null==x)w++;else if(null==q)y++;else if(x.key===q.key)w++,y++,h(e,x,q,b,z(l,w,c),p,d),p&&x.tag===q.tag&&n(e,m(x),c);else if(x=l[r],x!==q||p)if(null== -x)r--;else if(null==q)y++;else if(x.key===q.key)h(e,x,q,b,z(l,r+1,c),p,d),(p||y=w&&v>=y;){x=l[r];q=a[v];if(x!==q||p)if(null==x)r--;else{if(null!=q)if(x.key===q.key)h(e,x,q,b,z(l,r+1,c),p,d),p&&x.tag===q.tag&&n(e,m(x),c),null!=x.dom&&(c=x.dom),r--;else{if(!t){t=l;var x=r,C={},u;for(u=0;ub.indexOf("?")?"?":"&";b+=d+c}return b}function m(b){try{return""!==b?JSON.parse(b):null}catch(C){throw Error(b);}}function z(b){return b.responseText}function n(b,a){if("function"=== +typeof b)if(a instanceof Array)for(var c=0;cg.status||304===g.status)c(n(b.type,a));else{var d=Error(g.responseText),h;for(h in a)d[h]=a[h];f(d)}}catch(G){f(G)}};h&&null!=b.data?g.send(b.data):g.send()});return!0===b.background?r:v(r)},jsonp:function(b,h){var m=g();b=f(b,h);var r=new c(function(c,f){var g=b.callbackName|| +"_mithril_"+Math.round(1E16*Math.random())+"_"+t++,h=a.document.createElement("script");a[g]=function(d){h.parentNode.removeChild(h);c(n(b.type,d));delete a[g]};h.onerror=function(){h.parentNode.removeChild(h);f(Error("JSONP request failed"));delete a[g]};null==b.data&&(b.data={});b.url=d(b.url,b.data);b.data[b.callbackKey||"callback"]=g;h.src=k(b.url,b.data);a.document.documentElement.appendChild(h)});return!0===b.background?r:m(r)},setCompletionCallback:function(a){h=a}}}(window,"undefined"!==typeof Promise? +Promise:u),M=function(a){function c(e,l,a,b,c,d,f){for(;a=w&&v>=y;){var x=l[w],q=a[y];if(x!==q||p)if(null==x)w++;else if(null==q)y++;else if(x.key===q.key)w++,y++,k(e,x,q,b,z(l,w,d),p,f),p&&x.tag===q.tag&&n(e,m(x),d);else if(x=l[r],x!==q||p)if(null==x)r--;else if(null==q)y++;else if(x.key===q.key)k(e,x,q,b,z(l,r+1,d),p,f),(p||y=w&&v>=y;){x=l[r];q=a[v];if(x!==q||p)if(null==x)r--;else{if(null!= +q)if(x.key===q.key)k(e,x,q,b,z(l,r+1,d),p,f),p&&x.tag===q.tag&&n(e,m(x),d),null!=x.dom&&(d=x.dom),r--;else{if(!t){t=l;var x=r,C={},u;for(u=0;u