- From: Cameron McCormack <cam@mcc.id.au>
- Date: Fri, 16 Dec 2011 14:06:21 +1100
- To: Jonas Sicking <jonas@sicking.cc>
- CC: Ian Hickson <ian@hixie.ch>, Alex Russell <slightlyoff@google.com>, "public-script-coord@w3.org" <public-script-coord@w3.org>, Anne van Kesteren <annevk@opera.com>
I want to try to summarise the arguments we have so far. Like most
contentious Web IDL issues, this one revolves around what style we want
to encourage or allow. I think it's clear that there is nothing you can
do with object style or function style callbacks that you can't do with
the other.
We have the three options that Jonas mentioned:
a. accept only functions (except where required for web compatibility)
b. accept functions and objects with handleEvent
c. accept functions and objects with a descriptive function name
Let's consider an (untested) example with a combination of callback APIs
and callback source objects, with the three different approaches. The
example is a manager object that handles mouse events for dragging an
object around the canvas, and it also uses requestAnimationFrame to
animate the element while it's being dragged. I'm writing the example
under the assumption that we actually want to encapsulate the state
somewhere.
(I'm assuming in the example that each element we're managing has a
distinct ID.)
a. using objects with descriptive function names
------------------------------------------------
function isEmpty(set) {
for (var k in set) {
return false;
}
return true;
}
var manager = {
_elementState: { },
_animatingElements: { },
startManaging: function(element) {
element.addEventListener("mousedown", this, false);
element.addEventListener("mousemove", this, false);
element.addEventListener("mouseup", this, false);
this._elementState[element.id] = { };
},
stopManaging: function(element) {
element.removeEventListener("mousedown", this, false);
element.removeEventListener("mousemove", this, false);
element.removeEventListener("mouseup", this, false);
delete this._elementState[element.id];
},
handleEvent: function(event) {
if (event.type == "mousedown") {
this.handleMouseDown(event);
} else if (event.type == "mousemove") {
this.handleMouseMove(event);
} else if (event.type == "mouseup") {
this.handleMouseUp(event);
}
},
handleMouseDown: function(event) {
var element = event.target;
if (isEmpty(this._animatingElements)) {
requestAnimationFrame(this);
}
this._animatingElements[element.id] = true;
},
handleMouseMove: function(event) {
var element = event.target;
// move the element to the current drag position
// store the current drag position in this._elementState[element.id]
},
handleMouseUp: function(event) {
var element = event.target;
delete this._animatingElements[element.id];
},
sample: function(time) {
for (var id in this._animatingElements) {
var state = this._elementState[id];
// do animation for this element
}
if (!isEmpty(this._animatingElements)) {
requestAnimationFrame(this);
}
},
};
Since DOM Events routes all events through a single callback, our
handleEvent dispatches them to specific methods for particular event
types. It is easy to distinguish which element handleMouseDown etc. are
for since they can use the Event object passed in to look at the target.
So we can easily look up this._elementState[element.id] to get to the
state for the particular element being dragged.
requestAnimationFrame though doesn't identify what the source of the
callback is. So instead we need to record in a separate state variable
which elements are animating so that the single sample call can handle
all of them.
b. using objects with handleEvent
---------------------------------
function isEmpty(set) {
for (var k in set) {
return false;
}
return true;
}
var manager = {
_elementState: { },
_animatingElements: { },
startManaging: function(element) {
element.addEventListener("mousedown", this, false);
element.addEventListener("mousemove", this, false);
element.addEventListener("mouseup", this, false);
this._elementState[element.id] = { };
},
stopManaging: function(element) {
element.removeEventListener("mousedown", this, false);
element.removeEventListener("mousemove", this, false);
element.removeEventListener("mouseup", this, false);
delete this._elementState[element.id];
},
handleEvent: function(arg) {
if (typeof arg == "number") {
// This is the callback for requestAnimationFrame.
this.handleAnimationFrame(arg);
} else {
// This is the callback for addEventListener.
if (arg.type == "mousedown") {
this.handleMouseDown(event);
} else if (arg.type == "mousemove") {
this.handleMouseMove(event);
} else if (arg.type == "mouseup") {
this.handleMouseUp(event);
}
}
},
handleMouseDown: function(event) {
var element = event.target;
if (isEmpty(this._animatingElements)) {
requestAnimationFrame(this);
}
this._animatingElements[element.id] = true;
},
handleMouseMove: function(event) {
var element = event.target;
// move the element to the current drag position
// store the current drag position in this._elementState[element.id]
},
handleMouseUp: function(event) {
var element = event.target;
delete this._animatingElements[element.id];
},
handleAnimationFrame: function(time) {
for (var id in this._animatingElements) {
var state = this._elementState[id];
// do animation for this element
}
if (!isEmpty(this._animatingElements)) {
requestAnimationFrame(this);
}
},
};
So the only difference here is that inside handleEvent we now need to
detect whether it is being called due to requestAnimationFrame or due to
an event dispatch. In this case it is possible -- we can just check
whether the argument is a number or an Event object -- but in general
this is not always going to be possible. (I renamed the sample method
to handleAnimationFrame to point out that here you don't need to
remember that "sample" is the official method name, unlike in (a).)
c. using functions
------------------
var manager = {
_elementState: { },
init: function() {
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
},
startManaging: function(element) {
element.addEventListener("mousedown", this.handleMouseDown, false);
element.addEventListener("mousemove", this.handleMouseMove, false);
element.addEventListener("mouseup", this.handleMouseUp, false);
this._elementState[element.id] = { };
},
stopManaging: function(element) {
element.removeEventListener("mousedown", this.handleMouseDown,
false);
element.removeEventListener("mousemove", this.handleMouseMove,
false);
element.removeEventListener("mouseup", this.handleMouseUp, false);
delete this._elementState[element.id];
},
handleMouseDown: function(event) {
var element = event.target;
this._elementState[element.id].animating = true;
requestAnimationFrame(this.handleAnimationFrame.bind(this, element));
},
handleMouseMove: function(event) {
var element = event.target;
// move the element to the current drag position
// store the current drag position in this._elementState[element.id]
},
handleMouseUp: function(event) {
var element = event.target;
this._elementState[element.id].animating = false;
},
handleAnimationFrame: function(element, time) {
var state = this._elementState[element.id];
// do animation for this element
if (state.animating) {
requestAnimationFrame(this.handleAnimationFrame.bind(this,
element));
}
},
};
manager.init();
Using functions, we can now identify individual callback methods rather
than having all DOM Events callbacks go through handleEvent. That
eliminates our own discrimination/dispatching code. The downside is
that we need to have these callback functions bound to the manager
object, so that their "this" will refer to the manager. We do this in
the init function, so that we can call removeEventListener successfully
later.
(An alternative since this example is obviously a singleton object would
be to avoid binding them and just refer to "manager" instead of this.)
requestAnimationFrame is now also being passed a function rather than an
object. We can use bind() here to identify which element the animation
frame request is for, which means we don't need to coalesce all
animations into the one callback, which means we can do away with
_animatingElement, but we still do need to track which elements are
being animated. So handleMouseDown is a bit simpler, handleMouseUp is
about the same, and handleAnimationFrame is simpler.
Summary
-------
a. using objects with descriptive function names
Pros:
* listener registration calls are shorter than (c)
* different callback types can be handled without fragile sniffing
of arguments
* no storing of bound functions required for removeEventListener to
work
Cons:
* when looking at listener registration function, it is less clear
than (c) which function will be called
* for APIs like DOM Events, where multiple callback APIs are
routed through the same function, manual discrimination must be
done
* extra state needed to track callbacks where the source is not
identified, like requestAnimationFrame
* need to remember particular calback method name, like "sample"
* more complex implementation and more complex model to understand
for authors than "just function"
b. using objects with handleEvent
Pros:
* listener registration calls are shorter than (c)
* no need to remember particular callback method name, it is always
"handleEvent"
* no storing of bound functions required for removeEventListener to
work
Cons:
* when looking at listener registration function, it is less clear
than (c) which function will be called
* for APIs like DOM Events, where multiple callback APIs are
routed through the same function, manual discrimination must be
done
* extra state needed to track callbacks where the source is not
identified, like requestAnimationFrame
* fragile argument sniffing required inside handleEvent when
multiple callback types are handled
* more complex implementation and more complex model to understand
for authors than "just function"
c. using functions
Pros:
* no need to remember particular callback method name
* when looking at listener registration function, it is clearer
than (a) and (b) which function will be called
* for APIs like DOM Events, separate functions can be identified
per registration, meaning no manual discrimination required
* no extra state needed to track callbacks where the source is not
identified (like requestAnimationFrame), it can be encoded into
the identity of the callback function or into its argument list
with bind()
* simpler implementation and simpler model to understand for
authors than "function or object-with-property with defined
precedence between the two"
Cons:
* listener registration calls are longer than (a) and (c)
* result of bind() needs to be stored so that removeEventListener
works, and this can't be done nicely within the object
initialisation syntax
Another argument proponents of (a) or (b) could make is consistency with
existing APIs. For existing object-accepting callback APIs, we're
already inconsistent though; some use handleEvent, others use other
names. We also don't accept objects everywhere -- event listener
properties (onfoo) and setTimeout/setInterval come to mind.
People will weight the pros and cons differently, I imagine. But for me
based on the above I prefer (c).
Received on Friday, 16 December 2011 03:07:09 UTC