Make m.redraw() strictly asynchronous

This commit is contained in:
Pierre-Yves Gerardy 2017-02-06 00:58:16 +01:00 committed by Pierre-Yves Gérardy
parent 38956e119b
commit b004c20f0c
11 changed files with 281 additions and 111 deletions

View file

@ -16,6 +16,6 @@ module.exports = function(redrawService) {
redrawService.render(root, Vnode(component))
}
redrawService.subscribe(root, run)
redrawService.redraw()
run()
}
}

View file

@ -4,26 +4,23 @@ var coreRenderer = require("../render/render")
function throttle(callback) {
//60fps translates to 16.6ms, round it down since setTimeout requires int
var time = 16
var delay = 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) {
var elapsed = Date.now() - last
if (pending === null) {
pending = timeout(function() {
pending = null
callback()
last = Date.now()
}, time - (now - last))
}, delay - elapsed)
}
}
}
module.exports = function($window) {
module.exports = function($window, throttleMock) {
var _throttle = throttleMock || throttle
var renderService = coreRenderer($window)
renderService.setEventCallback(function(e) {
if (e.redraw !== false) redraw()
@ -32,7 +29,7 @@ module.exports = function($window) {
var callbacks = []
function subscribe(key, callback) {
unsubscribe(key)
callbacks.push(key, throttle(callback))
callbacks.push(key, _throttle(callback))
}
function unsubscribe(key) {
var index = callbacks.indexOf(key)

View file

@ -11,9 +11,14 @@ module.exports = function($window, redrawService) {
var render, component, attrs, currentPath, lastUpdate
var route = function(root, defaultRoute, routes) {
if (root == null) throw new Error("Ensure the DOM element that was passed to `m.route` is not undefined")
var run = function() {
function run() {
if (render != null) redrawService.render(root, render(Vnode(component, attrs.key, attrs)))
}
var redraw = function() {
run()
redraw = redrawService.redraw
}
redrawService.subscribe(root, run)
var bail = function(path) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute)
@ -24,7 +29,7 @@ module.exports = function($window, redrawService) {
component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs = params, currentPath = path, lastUpdate = null
render = (routeResolver.render || identity).bind(routeResolver)
run()
redraw()
}
if (payload.view || typeof payload === "function") update({}, payload)
else {
@ -36,7 +41,6 @@ module.exports = function($window, redrawService) {
else update(payload, "div")
}
}, bail)
redrawService.subscribe(root, run)
}
route.set = function(path, data, options) {
if (lastUpdate != null) options = {replace: true}

View file

@ -3,6 +3,7 @@
var o = require("../../ospec/ospec")
var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var coreRenderer = require("../../render/render")
@ -11,18 +12,22 @@ var apiMounter = require("../../api/mount")
o.spec("mount", function() {
var FRAME_BUDGET = Math.floor(1000 / 60)
var $window, root, redrawService, mount, render
var $window, root, redrawService, mount, render, throttleMock
o.beforeEach(function() {
$window = domMock()
throttleMock = throttleMocker()
root = $window.document.body
redrawService = apiRedraw($window)
redrawService = apiRedraw($window, throttleMock.throttle)
mount = apiMounter(redrawService)
render = coreRenderer($window).render
})
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid component", function() {
var threw = false
try {
@ -69,7 +74,7 @@ o.spec("mount", function() {
o(root.childNodes.length).equals(0)
})
o("redraws on events", function(done) {
o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@ -97,17 +102,12 @@ o.spec("mount", function() {
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
throttleMock.fire()
done()
}, FRAME_BUDGET)
o(onupdate.callCount).equals(1)
})
o("redraws several mount points on events", function(done, timeout) {
timeout(60)
o("redraws several mount points on events", function() {
var onupdate0 = o.spy()
var oninit0 = o.spy()
var onclick0 = o.spy()
@ -154,26 +154,26 @@ o.spec("mount", function() {
o(onclick0.callCount).equals(1)
o(onclick0.this).equals(root.childNodes[0].firstChild)
setTimeout(function() {
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
throttleMock.fire()
root.childNodes[1].firstChild.dispatchEvent(e)
o(onclick1.callCount).equals(1)
o(onclick1.this).equals(root.childNodes[1].firstChild)
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
setTimeout(function() {
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
root.childNodes[1].firstChild.dispatchEvent(e)
done()
}, FRAME_BUDGET)
}, FRAME_BUDGET)
o(onclick1.callCount).equals(1)
o(onclick1.this).equals(root.childNodes[1].firstChild)
throttleMock.fire()
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
})
o("event handlers can skip redraw", function(done) {
var onupdate = o.spy()
o("event handlers can skip redraw", function() {
var onupdate = o.spy(function(){
throw new Error("This shouldn't have been called")
})
var oninit = o.spy()
var e = $window.document.createEvent("MouseEvents")
@ -195,15 +195,12 @@ o.spec("mount", function() {
o(oninit.callCount).equals(1)
// Wrapped to ensure no redraw fired
setTimeout(function() {
o(onupdate.callCount).equals(0)
throttleMock.fire()
done()
}, FRAME_BUDGET)
o(onupdate.callCount).equals(0)
})
o("redraws when the render function is run", function(done) {
o("redraws when the render function is run", function() {
var onupdate = o.spy()
var oninit = o.spy()
@ -221,17 +218,12 @@ o.spec("mount", function() {
redrawService.redraw()
// Wrapped to give time for the rate-limited redraw to fire
setTimeout(function() {
o(onupdate.callCount).equals(1)
throttleMock.fire()
done()
}, FRAME_BUDGET)
o(onupdate.callCount).equals(1)
})
o("throttles", function(done, timeout) {
timeout(200)
o("throttles", function() {
var i = 0
mount(root, createComponent({view: function() {i++}}))
var before = i
@ -243,12 +235,11 @@ o.spec("mount", function() {
var after = i
setTimeout(function(){
o(before).equals(1) // mounts synchronously
o(after).equals(1) // throttles rest
o(i).equals(2)
done()
},40)
throttleMock.fire()
o(before).equals(1) // mounts synchronously
o(after).equals(1) // throttles rest
o(i).equals(2)
})
})
})

View file

@ -2,6 +2,7 @@
var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var apiRedraw = require("../../api/redraw")
o.spec("redrawService", function() {
@ -17,25 +18,39 @@ o.spec("redrawService", function() {
redrawService.redraw()
})
o("honours throttleMock", function() {
var throttleMock = throttleMocker()
redrawService = apiRedraw(domMock(), throttleMock.throttle)
var spy = o.spy()
redrawService.subscribe(root, spy)
o(spy.callCount).equals(0)
redrawService.redraw()
o(spy.callCount).equals(0)
throttleMock.fire()
o(spy.callCount).equals(1)
})
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)
redrawService.redraw()
redrawService.redraw()
redrawService.redraw()
o(spy.callCount).equals(1)
o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(2)
o(spy.callCount).equals(1)
done()
}, 20)
})
@ -54,27 +69,29 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
o(spy3.callCount).equals(0)
redrawService.redraw()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
o(spy3.callCount).equals(0)
setTimeout(function() {
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)
done()
}, 20)
})
o("should stop running after unsubscribe", function() {
var spy = o.spy()
o("should stop running after unsubscribe", function(done) {
var spy = o.spy(function() {
throw new Error("This shouldn't have been called")
})
redrawService.subscribe(root, spy)
redrawService.unsubscribe(root, spy)
@ -82,9 +99,14 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(0)
done()
}, 20)
})
o("does nothing on invalid unsubscribe", function() {
o("does nothing on invalid unsubscribe", function(done) {
var spy = o.spy()
redrawService.subscribe(root, spy)
@ -92,6 +114,10 @@ o.spec("redrawService", function() {
redrawService.redraw()
o(spy.callCount).equals(1)
setTimeout(function() {
o(spy.callCount).equals(1)
done()
}, 20)
})
})

View file

@ -3,6 +3,7 @@
var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync")
var browserMock = require("../../test-utils/browserMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript")
var callAsync = require("../../test-utils/callAsync")
@ -15,18 +16,23 @@ o.spec("route", function() {
void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) {
o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() {
var FRAME_BUDGET = Math.floor(1000 / 60)
var $window, root, redrawService, route
var $window, root, redrawService, route, throttleMock
o.beforeEach(function() {
$window = browserMock(env)
throttleMock = throttleMocker()
root = $window.document.body
redrawService = apiRedraw($window)
redrawService = apiRedraw($window, throttleMock.throttle)
route = apiRouter($window, redrawService)
route.prefix(prefix)
})
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid `root` DOM node", function() {
var threw = false
try {
@ -50,7 +56,7 @@ o.spec("route", function() {
o(root.firstChild.nodeName).equals("DIV")
})
o("routed mount points can redraw synchronously (POJO component)", function() {
o("routed mount points only redraw asynchronously (POJO component)", function() {
var view = o.spy()
$window.location.href = prefix + "/"
@ -60,11 +66,14 @@ o.spec("route", function() {
redrawService.redraw()
o(view.callCount).equals(2)
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(2)
})
o("routed mount points can redraw synchronously (constructible component)", function() {
o("routed mount points only redraw asynchronously (constructible component)", function() {
var view = o.spy()
var Cmp = function(){}
@ -77,11 +86,14 @@ o.spec("route", function() {
redrawService.redraw()
o(view.callCount).equals(2)
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(2)
})
o("routed mount points can redraw synchronously (closure component)", function() {
o("routed mount points only redraw asynchronously (closure component)", function() {
var view = o.spy()
function Cmp() {return {view: view}}
@ -93,8 +105,11 @@ o.spec("route", function() {
redrawService.redraw()
o(view.callCount).equals(2)
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(2)
})
o("default route doesn't break back button", function(done) {
@ -160,11 +175,12 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("redraws on events", function(done) {
o("redraws on events", function() {
var onupdate = o.spy()
var oninit = o.spy()
var onclick = o.spy()
@ -194,12 +210,9 @@ o.spec("route", function() {
o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire
callAsync(function() {
o(onupdate.callCount).equals(1)
done()
})
throttleMock.fire()
o(onupdate.callCount).equals(1)
})
o("event handlers can skip redraw", function(done) {
@ -500,7 +513,10 @@ o.spec("route", function() {
o(oninit.callCount).equals(1)
route.set("/def")
callAsync(function() {
throttleMock.fire()
o(oninit.callCount).equals(2)
done()
})
})
@ -536,23 +552,28 @@ o.spec("route", function() {
route(root, "/a", {
"/a" : {
render: function() {
return m("div")
return m("div", m('p'))
},
},
"/b" : {
render: function() {
return m("div")
return m("div", m('a'))
},
},
})
var dom = root.firstChild
var child = dom.firstChild
o(root.firstChild.nodeName).equals("DIV")
route.set("/b")
callAsync(function() {
throttleMock.fire()
o(root.firstChild).equals(dom)
o(root.firstChild.firstChild).notEquals(child)
done()
})
@ -586,6 +607,7 @@ o.spec("route", function() {
o(renderCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(matchCount).equals(1)
o(renderCount).equals(2)
@ -621,6 +643,7 @@ o.spec("route", function() {
o(renderCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(matchCount).equals(1)
o(renderCount).equals(2)
@ -815,10 +838,14 @@ o.spec("route", function() {
})
callAsync(function() {
route.set("/b")
throttleMock.fire()
route.set('/b')
callAsync(function() {
callAsync(function() {
callAsync(function() {
throttleMock.fire()
o(render.callCount).equals(0)
o(component.view.callCount).equals(2)
@ -939,6 +966,7 @@ o.spec("route", function() {
o(onmatch.callCount).equals(1)
redrawService.redraw()
throttleMock.fire()
o(view.callCount).equals(2)
o(onmatch.callCount).equals(1)
@ -1017,6 +1045,8 @@ o.spec("route", function() {
})
callAsync(function() {
throttleMock.fire()
o(onmatch.callCount).equals(1)
o(render.callCount).equals(1)
@ -1024,6 +1054,8 @@ o.spec("route", function() {
callAsync(function() {
callAsync(function() {
throttleMock.fire()
o(onmatch.callCount).equals(2)
o(render.callCount).equals(2)
@ -1074,9 +1106,15 @@ o.spec("route", function() {
route.set("/b")
callAsync(function() {
route.set("/a")
throttleMock.fire()
o(root.firstChild.nodeName).equals("B")
route.set('/a')
callAsync(function() {
throttleMock.fire()
o(root.firstChild.nodeName).equals("A")
done()
@ -1141,7 +1179,9 @@ o.spec("route", function() {
route.set("/b")
// setting the route is asynchronous
callAsync(function() {
throttleMock.fire()
o(spy.callCount).equals(1)
done()
@ -1181,9 +1221,7 @@ o.spec("route", function() {
})
})
o("throttles", function(done, timeout) {
timeout(200)
o("throttles", function() {
var i = 0
$window.location.href = prefix + "/"
route(root, "/", {
@ -1197,12 +1235,11 @@ o.spec("route", function() {
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()
}, FRAME_BUDGET * 2)
throttleMock.fire()
o(before).equals(1) // routes synchronously
o(after).equals(1) // redraws asynchronously
o(i).equals(2)
})
o("m.route.param is available outside of route handlers", function(done) {