Skip to content

Commit

Permalink
Merge pull request #1592 from pygy/async-redraw
Browse files Browse the repository at this point in the history
Make m.redraw() purely asynchronous, add m.redraw.sync()
  • Loading branch information
pygy authored Jul 17, 2017
2 parents c7d72ba + 0e0ed7c commit 8ab3179
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 114 deletions.
2 changes: 1 addition & 1 deletion api/mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ module.exports = function(redrawService) {
redrawService.render(root, Vnode(component))
}
redrawService.subscribe(root, run)
redrawService.redraw()
run()
}
}
31 changes: 17 additions & 14 deletions api/redraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,48 @@ 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 renderService = coreRenderer($window)
renderService.setEventCallback(function(e) {
if (e.redraw === false) e.redraw = undefined
else redraw()
})

var callbacks = []
var rendering = false

function subscribe(key, callback) {
unsubscribe(key)
callbacks.push(key, throttle(callback))
callbacks.push(key, callback)
}
function unsubscribe(key) {
var index = callbacks.indexOf(key)
if (index > -1) callbacks.splice(index, 2)
}
function redraw() {
for (var i = 1; i < callbacks.length; i += 2) {
callbacks[i]()
}
function sync() {
if (rendering) throw new Error("Nested m.redraw.sync() call")
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}
}
10 changes: 7 additions & 3 deletions api/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down
116 changes: 68 additions & 48 deletions api/tests/test-mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,29 @@
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 apiRedraw = require("../../api/redraw")
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 = redrawService.render
})

o.afterEach(function() {
o(throttleMock.queueLength()).equals(0)
})

o("throws on invalid component", function() {
var threw = false
try {
Expand All @@ -46,7 +50,7 @@ o.spec("mount", function() {
o(threw).equals(true)
})

o("renders into `root`", function() {
o("renders into `root` synchronoulsy", function() {
mount(root, createComponent({
view : function() {
return m("div")
Expand All @@ -68,7 +72,37 @@ o.spec("mount", function() {
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 oninit = o.spy()
var onclick = o.spy()
Expand Down Expand Up @@ -96,17 +130,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()
Expand Down Expand Up @@ -153,26 +182,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")

Expand All @@ -194,15 +223,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()

Expand All @@ -220,17 +246,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
Expand All @@ -242,12 +263,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)
})
})
})
Expand Down
Loading

0 comments on commit 8ab3179

Please sign in to comment.