- 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