Merge pull request #1592 from pygy/async-redraw

Make m.redraw() purely asynchronous, add m.redraw.sync()
This commit is contained in:
Pierre-Yves Gérardy 2017-07-17 23:16:44 +02:00 committed by GitHub
commit 8ab31790ab
11 changed files with 381 additions and 116 deletions

View file

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

View file

@ -4,26 +4,23 @@ var coreRenderer = require("../render/render")
function throttle(callback) { function throttle(callback) {
//60fps translates to 16.6ms, round it down since setTimeout requires int //60fps translates to 16.6ms, round it down since setTimeout requires int
var time = 16 var delay = 16
var last = 0, pending = null var last = 0, pending = null
var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout var timeout = typeof requestAnimationFrame === "function" ? requestAnimationFrame : setTimeout
return function() { return function() {
var now = Date.now() var elapsed = Date.now() - last
if (last === 0 || now - last >= time) { if (pending === null) {
last = now
callback()
}
else if (pending === null) {
pending = timeout(function() { pending = timeout(function() {
pending = null pending = null
callback() callback()
last = Date.now() last = Date.now()
}, time - (now - last)) }, delay - elapsed)
} }
} }
} }
module.exports = function($window) {
module.exports = function($window, throttleMock) {
var renderService = coreRenderer($window) var renderService = coreRenderer($window)
renderService.setEventCallback(function(e) { renderService.setEventCallback(function(e) {
if (e.redraw === false) e.redraw = undefined if (e.redraw === false) e.redraw = undefined
@ -31,18 +28,24 @@ module.exports = function($window) {
}) })
var callbacks = [] var callbacks = []
var rendering = false
function subscribe(key, callback) { function subscribe(key, callback) {
unsubscribe(key) unsubscribe(key)
callbacks.push(key, throttle(callback)) callbacks.push(key, callback)
} }
function unsubscribe(key) { function unsubscribe(key) {
var index = callbacks.indexOf(key) var index = callbacks.indexOf(key)
if (index > -1) callbacks.splice(index, 2) if (index > -1) callbacks.splice(index, 2)
} }
function redraw() { function sync() {
for (var i = 1; i < callbacks.length; i += 2) { if (rendering) throw new Error("Nested m.redraw.sync() call")
callbacks[i]() rendering = true
} for (var i = 1; i < callbacks.length; i+=2) try {callbacks[i]()} catch (e) {/*noop*/}
rendering = false
} }
var redraw = (throttleMock || throttle)(sync)
redraw.sync = sync
return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render} return {subscribe: subscribe, unsubscribe: unsubscribe, redraw: redraw, render: renderService.render}
} }

View file

@ -11,9 +11,14 @@ module.exports = function($window, redrawService) {
var render, component, attrs, currentPath, lastUpdate var render, component, attrs, currentPath, lastUpdate
var route = function(root, defaultRoute, routes) { 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") 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))) 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) { var bail = function(path) {
if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true}) if (path !== defaultRoute) routeService.setPath(defaultRoute, null, {replace: true})
else throw new Error("Could not resolve default route " + defaultRoute) 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" component = comp != null && (typeof comp.view === "function" || typeof comp === "function")? comp : "div"
attrs = params, currentPath = path, lastUpdate = null attrs = params, currentPath = path, lastUpdate = null
render = (routeResolver.render || identity).bind(routeResolver) render = (routeResolver.render || identity).bind(routeResolver)
run() redraw()
} }
if (payload.view || typeof payload === "function") update({}, payload) if (payload.view || typeof payload === "function") update({}, payload)
else { else {
@ -36,7 +41,6 @@ module.exports = function($window, redrawService) {
else update(payload, "div") else update(payload, "div")
} }
}, bail) }, bail)
redrawService.subscribe(root, run)
} }
route.set = function(path, data, options) { route.set = function(path, data, options) {
if (lastUpdate != null) { if (lastUpdate != null) {

View file

@ -3,25 +3,29 @@
var o = require("../../ospec/ospec") var o = require("../../ospec/ospec")
var components = require("../../test-utils/components") var components = require("../../test-utils/components")
var domMock = require("../../test-utils/domMock") var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript") var m = require("../../render/hyperscript")
var apiRedraw = require("../../api/redraw") var apiRedraw = require("../../api/redraw")
var apiMounter = require("../../api/mount") var apiMounter = require("../../api/mount")
o.spec("mount", function() { o.spec("mount", function() {
var FRAME_BUDGET = Math.floor(1000 / 60) var $window, root, redrawService, mount, render, throttleMock
var $window, root, redrawService, mount, render
o.beforeEach(function() { o.beforeEach(function() {
$window = domMock() $window = domMock()
throttleMock = throttleMocker()
root = $window.document.body root = $window.document.body
redrawService = apiRedraw($window, throttleMock.throttle)
redrawService = apiRedraw($window)
mount = apiMounter(redrawService) mount = apiMounter(redrawService)
render = redrawService.render render = redrawService.render
}) })
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid component", function() { o("throws on invalid component", function() {
var threw = false var threw = false
try { try {
@ -46,7 +50,7 @@ o.spec("mount", function() {
o(threw).equals(true) o(threw).equals(true)
}) })
o("renders into `root`", function() { o("renders into `root` synchronoulsy", function() {
mount(root, createComponent({ mount(root, createComponent({
view : function() { view : function() {
return m("div") return m("div")
@ -68,7 +72,37 @@ o.spec("mount", function() {
o(root.childNodes.length).equals(0) o(root.childNodes.length).equals(0)
}) })
o("redraws on events", function(done) { o("Mounting a second root doesn't cause the first one to redraw", function() {
var view = o.spy(function() {
return m("div")
})
render(root, [
m("#child0"),
m("#child1")
])
mount(root.childNodes[0], createComponent({
view : view
}))
o(root.firstChild.nodeName).equals("DIV")
o(view.callCount).equals(1)
mount(root.childNodes[1], createComponent({
view : function() {
return m("div")
}
}))
o(view.callCount).equals(1)
throttleMock.fire()
o(view.callCount).equals(1)
})
o("redraws on events", function() {
var onupdate = o.spy() var onupdate = o.spy()
var oninit = o.spy() var oninit = o.spy()
var onclick = o.spy() var onclick = o.spy()
@ -96,17 +130,12 @@ o.spec("mount", function() {
o(onclick.args[0].type).equals("click") o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild) o(onclick.args[0].target).equals(root.firstChild)
// Wrapped to give time for the rate-limited redraw to fire throttleMock.fire()
setTimeout(function() {
o(onupdate.callCount).equals(1)
done() o(onupdate.callCount).equals(1)
}, FRAME_BUDGET)
}) })
o("redraws several mount points on events", function(done, timeout) { o("redraws several mount points on events", function() {
timeout(60)
var onupdate0 = o.spy() var onupdate0 = o.spy()
var oninit0 = o.spy() var oninit0 = o.spy()
var onclick0 = o.spy() var onclick0 = o.spy()
@ -153,26 +182,26 @@ o.spec("mount", function() {
o(onclick0.callCount).equals(1) o(onclick0.callCount).equals(1)
o(onclick0.this).equals(root.childNodes[0].firstChild) o(onclick0.this).equals(root.childNodes[0].firstChild)
setTimeout(function() { throttleMock.fire()
o(onupdate0.callCount).equals(1)
o(onupdate1.callCount).equals(1)
root.childNodes[1].firstChild.dispatchEvent(e) o(onupdate0.callCount).equals(1)
o(onclick1.callCount).equals(1) o(onupdate1.callCount).equals(1)
o(onclick1.this).equals(root.childNodes[1].firstChild)
setTimeout(function() { root.childNodes[1].firstChild.dispatchEvent(e)
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
done() o(onclick1.callCount).equals(1)
}, FRAME_BUDGET) o(onclick1.this).equals(root.childNodes[1].firstChild)
}, FRAME_BUDGET)
throttleMock.fire()
o(onupdate0.callCount).equals(2)
o(onupdate1.callCount).equals(2)
}) })
o("event handlers can skip redraw", function(done) { o("event handlers can skip redraw", function() {
var onupdate = o.spy() var onupdate = o.spy(function(){
throw new Error("This shouldn't have been called")
})
var oninit = o.spy() var oninit = o.spy()
var e = $window.document.createEvent("MouseEvents") var e = $window.document.createEvent("MouseEvents")
@ -194,15 +223,12 @@ o.spec("mount", function() {
o(oninit.callCount).equals(1) o(oninit.callCount).equals(1)
// Wrapped to ensure no redraw fired throttleMock.fire()
setTimeout(function() {
o(onupdate.callCount).equals(0)
done() o(onupdate.callCount).equals(0)
}, FRAME_BUDGET)
}) })
o("redraws when the render function is run", function(done) { o("redraws when the render function is run", function() {
var onupdate = o.spy() var onupdate = o.spy()
var oninit = o.spy() var oninit = o.spy()
@ -220,17 +246,12 @@ o.spec("mount", function() {
redrawService.redraw() redrawService.redraw()
// Wrapped to give time for the rate-limited redraw to fire throttleMock.fire()
setTimeout(function() {
o(onupdate.callCount).equals(1)
done() o(onupdate.callCount).equals(1)
}, FRAME_BUDGET)
}) })
o("throttles", function(done, timeout) { o("throttles", function() {
timeout(200)
var i = 0 var i = 0
mount(root, createComponent({view: function() {i++}})) mount(root, createComponent({view: function() {i++}}))
var before = i var before = i
@ -242,12 +263,11 @@ o.spec("mount", function() {
var after = i var after = i
setTimeout(function(){ throttleMock.fire()
o(before).equals(1) // mounts synchronously
o(after).equals(1) // throttles rest o(before).equals(1) // mounts synchronously
o(i).equals(2) o(after).equals(1) // throttles rest
done() o(i).equals(2)
},40)
}) })
}) })
}) })

View file

@ -2,6 +2,7 @@
var o = require("../../ospec/ospec") var o = require("../../ospec/ospec")
var domMock = require("../../test-utils/domMock") var domMock = require("../../test-utils/domMock")
var throttleMocker = require("../../test-utils/throttleMock")
var apiRedraw = require("../../api/redraw") var apiRedraw = require("../../api/redraw")
o.spec("redrawService", function() { o.spec("redrawService", function() {
@ -17,25 +18,39 @@ o.spec("redrawService", function() {
redrawService.redraw() 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) { o("should run a single renderer entry", function(done) {
var spy = o.spy() var spy = o.spy()
redrawService.subscribe(root, spy) redrawService.subscribe(root, spy)
o(spy.callCount).equals(0) o(spy.callCount).equals(0)
redrawService.redraw()
o(spy.callCount).equals(1)
redrawService.redraw() redrawService.redraw()
redrawService.redraw() redrawService.redraw()
redrawService.redraw() redrawService.redraw()
o(spy.callCount).equals(1) o(spy.callCount).equals(0)
setTimeout(function() { setTimeout(function() {
o(spy.callCount).equals(2) o(spy.callCount).equals(1)
done() done()
}, 20) }, 20)
}) })
@ -54,27 +69,29 @@ o.spec("redrawService", function() {
redrawService.redraw() redrawService.redraw()
o(spy1.callCount).equals(1) o(spy1.callCount).equals(0)
o(spy2.callCount).equals(1) o(spy2.callCount).equals(0)
o(spy3.callCount).equals(1) o(spy3.callCount).equals(0)
redrawService.redraw() redrawService.redraw()
o(spy1.callCount).equals(1) o(spy1.callCount).equals(0)
o(spy2.callCount).equals(1) o(spy2.callCount).equals(0)
o(spy3.callCount).equals(1) o(spy3.callCount).equals(0)
setTimeout(function() { setTimeout(function() {
o(spy1.callCount).equals(2) o(spy1.callCount).equals(1)
o(spy2.callCount).equals(2) o(spy2.callCount).equals(1)
o(spy3.callCount).equals(2) o(spy3.callCount).equals(1)
done() done()
}, 20) }, 20)
}) })
o("should stop running after unsubscribe", function() { o("should stop running after unsubscribe", function(done) {
var spy = o.spy() var spy = o.spy(function() {
throw new Error("This shouldn't have been called")
})
redrawService.subscribe(root, spy) redrawService.subscribe(root, spy)
redrawService.unsubscribe(root, spy) redrawService.unsubscribe(root, spy)
@ -82,9 +99,33 @@ o.spec("redrawService", function() {
redrawService.redraw() redrawService.redraw()
o(spy.callCount).equals(0) o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(0)
done()
}, 20)
}) })
o("does nothing on invalid unsubscribe", function() { o("should stop running after unsubscribe, even if it occurs after redraw is requested", function(done) {
var spy = o.spy(function() {
throw new Error("This shouldn't have been called")
})
redrawService.subscribe(root, spy)
redrawService.redraw()
redrawService.unsubscribe(root, spy)
o(spy.callCount).equals(0)
setTimeout(function() {
o(spy.callCount).equals(0)
done()
}, 20)
})
o("does nothing on invalid unsubscribe", function(done) {
var spy = o.spy() var spy = o.spy()
redrawService.subscribe(root, spy) redrawService.subscribe(root, spy)
@ -92,6 +133,39 @@ o.spec("redrawService", function() {
redrawService.redraw() redrawService.redraw()
o(spy.callCount).equals(1) setTimeout(function() {
o(spy.callCount).equals(1)
done()
}, 20)
})
o("redraw.sync() redraws all roots synchronously", function() {
var el1 = $document.createElement("div")
var el2 = $document.createElement("div")
var el3 = $document.createElement("div")
var spy1 = o.spy()
var spy2 = o.spy()
var spy3 = o.spy()
redrawService.subscribe(el1, spy1)
redrawService.subscribe(el2, spy2)
redrawService.subscribe(el3, spy3)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
o(spy3.callCount).equals(0)
redrawService.redraw.sync()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
o(spy3.callCount).equals(1)
redrawService.redraw.sync()
o(spy1.callCount).equals(2)
o(spy2.callCount).equals(2)
o(spy3.callCount).equals(2)
}) })
}) })

View file

@ -3,6 +3,7 @@
var o = require("../../ospec/ospec") var o = require("../../ospec/ospec")
var callAsync = require("../../test-utils/callAsync") var callAsync = require("../../test-utils/callAsync")
var browserMock = require("../../test-utils/browserMock") var browserMock = require("../../test-utils/browserMock")
var throttleMocker = require("../../test-utils/throttleMock")
var m = require("../../render/hyperscript") var m = require("../../render/hyperscript")
var callAsync = require("../../test-utils/callAsync") var callAsync = require("../../test-utils/callAsync")
@ -14,19 +15,23 @@ o.spec("route", function() {
void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) { void [{protocol: "http:", hostname: "localhost"}, {protocol: "file:", hostname: "/"}].forEach(function(env) {
void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) { void ["#", "?", "", "#!", "?!", "/foo"].forEach(function(prefix) {
o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() { o.spec("using prefix `" + prefix + "` starting on " + env.protocol + "//" + env.hostname, function() {
var FRAME_BUDGET = Math.floor(1000 / 60) var $window, root, redrawService, route, throttleMock
var $window, root, redrawService, route
o.beforeEach(function() { o.beforeEach(function() {
$window = browserMock(env) $window = browserMock(env)
throttleMock = throttleMocker()
root = $window.document.body root = $window.document.body
redrawService = apiRedraw($window) redrawService = apiRedraw($window, throttleMock.throttle)
route = apiRouter($window, redrawService) route = apiRouter($window, redrawService)
route.prefix(prefix) route.prefix(prefix)
}) })
o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})
o("throws on invalid `root` DOM node", function() { o("throws on invalid `root` DOM node", function() {
var threw = false var threw = false
try { try {
@ -50,7 +55,7 @@ o.spec("route", function() {
o(root.firstChild.nodeName).equals("DIV") 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() var view = o.spy()
$window.location.href = prefix + "/" $window.location.href = prefix + "/"
@ -60,11 +65,14 @@ o.spec("route", function() {
redrawService.redraw() 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 view = o.spy()
var Cmp = function(){} var Cmp = function(){}
@ -77,11 +85,14 @@ o.spec("route", function() {
redrawService.redraw() 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() var view = o.spy()
function Cmp() {return {view: view}} function Cmp() {return {view: view}}
@ -93,8 +104,11 @@ o.spec("route", function() {
redrawService.redraw() 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) { o("default route doesn't break back button", function(done) {
@ -160,11 +174,12 @@ o.spec("route", function() {
o(oninit.callCount).equals(1) o(oninit.callCount).equals(1)
redrawService.redraw() redrawService.redraw()
throttleMock.fire()
o(onupdate.callCount).equals(1) o(onupdate.callCount).equals(1)
}) })
o("redraws on events", function(done) { o("redraws on events", function() {
var onupdate = o.spy() var onupdate = o.spy()
var oninit = o.spy() var oninit = o.spy()
var onclick = o.spy() var onclick = o.spy()
@ -194,12 +209,9 @@ o.spec("route", function() {
o(onclick.args[0].type).equals("click") o(onclick.args[0].type).equals("click")
o(onclick.args[0].target).equals(root.firstChild) 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) { o("event handlers can skip redraw", function(done) {
@ -502,7 +514,10 @@ o.spec("route", function() {
o(oninit.callCount).equals(1) o(oninit.callCount).equals(1)
route.set("/def") route.set("/def")
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(oninit.callCount).equals(2) o(oninit.callCount).equals(2)
done() done()
}) })
}) })
@ -538,23 +553,28 @@ o.spec("route", function() {
route(root, "/a", { route(root, "/a", {
"/a" : { "/a" : {
render: function() { render: function() {
return m("div") return m("div", m("p"))
}, },
}, },
"/b" : { "/b" : {
render: function() { render: function() {
return m("div") return m("div", m("a"))
}, },
}, },
}) })
var dom = root.firstChild var dom = root.firstChild
var child = dom.firstChild
o(root.firstChild.nodeName).equals("DIV") o(root.firstChild.nodeName).equals("DIV")
route.set("/b") route.set("/b")
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(root.firstChild).equals(dom) o(root.firstChild).equals(dom)
o(root.firstChild.firstChild).notEquals(child)
done() done()
}) })
@ -588,6 +608,7 @@ o.spec("route", function() {
o(renderCount).equals(1) o(renderCount).equals(1)
redrawService.redraw() redrawService.redraw()
throttleMock.fire()
o(matchCount).equals(1) o(matchCount).equals(1)
o(renderCount).equals(2) o(renderCount).equals(2)
@ -623,6 +644,7 @@ o.spec("route", function() {
o(renderCount).equals(1) o(renderCount).equals(1)
redrawService.redraw() redrawService.redraw()
throttleMock.fire()
o(matchCount).equals(1) o(matchCount).equals(1)
o(renderCount).equals(2) o(renderCount).equals(2)
@ -818,10 +840,14 @@ o.spec("route", function() {
}) })
callAsync(function() { callAsync(function() {
throttleMock.fire()
route.set("/b") route.set("/b")
callAsync(function() { callAsync(function() {
callAsync(function() { callAsync(function() {
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(render.callCount).equals(0) o(render.callCount).equals(0)
o(component.view.callCount).equals(2) o(component.view.callCount).equals(2)
@ -942,6 +968,7 @@ o.spec("route", function() {
o(onmatch.callCount).equals(1) o(onmatch.callCount).equals(1)
redrawService.redraw() redrawService.redraw()
throttleMock.fire()
o(view.callCount).equals(2) o(view.callCount).equals(2)
o(onmatch.callCount).equals(1) o(onmatch.callCount).equals(1)
@ -1020,6 +1047,8 @@ o.spec("route", function() {
}) })
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(onmatch.callCount).equals(1) o(onmatch.callCount).equals(1)
o(render.callCount).equals(1) o(render.callCount).equals(1)
@ -1027,6 +1056,8 @@ o.spec("route", function() {
callAsync(function() { callAsync(function() {
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(onmatch.callCount).equals(2) o(onmatch.callCount).equals(2)
o(render.callCount).equals(2) o(render.callCount).equals(2)
@ -1077,9 +1108,15 @@ o.spec("route", function() {
route.set("/b") route.set("/b")
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(root.firstChild.nodeName).equals("B")
route.set("/a") route.set("/a")
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(root.firstChild.nodeName).equals("A") o(root.firstChild.nodeName).equals("A")
done() done()
@ -1144,7 +1181,9 @@ o.spec("route", function() {
route.set("/b") route.set("/b")
// setting the route is asynchronous
callAsync(function() { callAsync(function() {
throttleMock.fire()
o(spy.callCount).equals(1) o(spy.callCount).equals(1)
done() done()
@ -1184,9 +1223,7 @@ o.spec("route", function() {
}) })
}) })
o("throttles", function(done, timeout) { o("throttles", function() {
timeout(200)
var i = 0 var i = 0
$window.location.href = prefix + "/" $window.location.href = prefix + "/"
route(root, "/", { route(root, "/", {
@ -1200,12 +1237,11 @@ o.spec("route", function() {
redrawService.redraw() redrawService.redraw()
var after = i var after = i
setTimeout(function() { throttleMock.fire()
o(before).equals(1) // routes synchronously
o(after).equals(2) // redraws synchronously o(before).equals(1) // routes synchronously
o(i).equals(3) // throttles rest o(after).equals(1) // redraws asynchronously
done() o(i).equals(2)
}, FRAME_BUDGET * 2)
}) })
o("m.route.param is available outside of route handlers", function(done) { o("m.route.param is available outside of route handlers", function(done) {

View file

@ -14,7 +14,7 @@ You DON'T need to call it if data is modified within the execution context of an
You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries. You DO need to call it in `setTimeout`/`setInterval`/`requestAnimationFrame` callbacks, or callbacks from 3rd party libraries.
Typically, `m.redraw` triggers an asynchronous redraws, but it may trigger synchronously if Mithril detects it's possible to improve performance by doing so (i.e. if no redraw was requested within the last animation frame). You should write code assuming that it always redraws asynchronously. `m.redraw` always triggers an asynchronous redraws.
--- ---

View file

@ -65,7 +65,7 @@ Argument | Type | Required | D
##### m.route.set ##### m.route.set
Redirects to a matching route, or to the default route if no matching routes can be found. Redirects to a matching route, or to the default route if no matching routes can be found. Triggers an asynchronous redraw off all mount points.
`m.route.set(path, data, options)` `m.route.set(path, data, options)`

View file

@ -0,0 +1,91 @@
"use strict"
var o = require("../../ospec/ospec")
var throttleMocker = require("../../test-utils/throttleMock")
o.spec("throttleMock", function() {
o("works with one callback", function() {
var throttleMock = throttleMocker()
var spy = o.spy()
o(throttleMock.queueLength()).equals(0)
var throttled = throttleMock.throttle(spy)
o(throttleMock.queueLength()).equals(0)
o(spy.callCount).equals(0)
throttled()
o(throttleMock.queueLength()).equals(1)
o(spy.callCount).equals(0)
throttled()
o(throttleMock.queueLength()).equals(1)
o(spy.callCount).equals(0)
throttleMock.fire()
o(throttleMock.queueLength()).equals(0)
o(spy.callCount).equals(1)
throttleMock.fire()
o(spy.callCount).equals(1)
})
o("works with two callbacks", function() {
var throttleMock = throttleMocker()
var spy1 = o.spy()
var spy2 = o.spy()
o(throttleMock.queueLength()).equals(0)
var throttled1 = throttleMock.throttle(spy1)
o(throttleMock.queueLength()).equals(0)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled1()
o(throttleMock.queueLength()).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled1()
o(throttleMock.queueLength()).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
var throttled2 = throttleMock.throttle(spy2)
o(throttleMock.queueLength()).equals(1)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled2()
o(throttleMock.queueLength()).equals(2)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttled2()
o(throttleMock.queueLength()).equals(2)
o(spy1.callCount).equals(0)
o(spy2.callCount).equals(0)
throttleMock.fire()
o(throttleMock.queueLength()).equals(0)
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
throttleMock.fire()
o(spy1.callCount).equals(1)
o(spy2.callCount).equals(1)
})
})

View file

@ -0,0 +1,27 @@
"use strict"
module.exports = function() {
var queue = []
return {
throttle: function(fn) {
var pending = false
return function() {
if (!pending) {
queue.push(function(){
pending = false
fn()
})
pending = true
}
}
},
fire: function() {
var tasks = queue
queue = []
tasks.forEach(function(fn) {fn()})
},
queueLength: function(){
return queue.length
}
}
}

View file

@ -163,14 +163,24 @@ o.spec("api", function() {
var count = 0 var count = 0
var root = window.document.createElement("div") var root = window.document.createElement("div")
m.mount(root, createComponent({view: function() {count++}})) m.mount(root, createComponent({view: function() {count++}}))
o(count).equals(1)
m.redraw()
o(count).equals(1)
setTimeout(function() { setTimeout(function() {
m.redraw()
o(count).equals(2) o(count).equals(2)
done() done()
}, FRAME_BUDGET) }, FRAME_BUDGET)
}) })
o("sync", function() {
var root = window.document.createElement("div")
var view = o.spy()
m.mount(root, createComponent({view: view}))
o(view.callCount).equals(1)
m.redraw.sync()
o(view.callCount).equals(2)
})
}) })
}) })
}) })