Path: csiph.com!v102.xanadu-bbs.net!xanadu-bbs.net!news.glorb.com!npeer02.iad.highwinds-media.com!news.highwinds-media.com!feed-me.highwinds-media.com!postnews.google.com!glegroupsg2000goo.googlegroups.com!not-for-mail From: Adam Silver Newsgroups: comp.lang.javascript Subject: Re: David Mark's Essential Javascript Tips - Volume #8 - Tip #47E - Attaching and Detaching Event Listeners Date: Wed, 14 Mar 2012 07:04:10 -0700 (PDT) Organization: http://groups.google.com Lines: 346 Message-ID: <46382.214.1331733850767.JavaMail.geo-discussion-forums@vbel7> References: <6d565e55-dae8-4056-870c-8c3340a8e992@u32g2000yqe.googlegroups.com> NNTP-Posting-Host: 195.89.39.30 Mime-Version: 1.0 Content-Type: text/plain; charset=ISO-8859-1 X-Trace: posting.google.com 1331734810 2537 127.0.0.1 (14 Mar 2012 14:20:10 GMT) X-Complaints-To: groups-abuse@google.com NNTP-Posting-Date: Wed, 14 Mar 2012 14:20:10 +0000 (UTC) In-Reply-To: <6d565e55-dae8-4056-870c-8c3340a8e992@u32g2000yqe.googlegroups.com> Complaints-To: groups-abuse@google.com Injection-Info: glegroupsg2000goo.googlegroups.com; posting-host=195.89.39.30; posting-account=TnwsOAoAAAAmZNTKKeHoK5eXwzEA_SLD User-Agent: G2/1.0 X-Received-Bytes: 14606 Xref: csiph.com comp.lang.javascript:12700 Thanks David, We have most of this on the Jessie project now. On Thursday, December 15, 2011 1:09:58 AM UTC, David Mark wrote: > Attaching and Detaching Event Listeners > > Attaching (and to a lesser extent detaching) event listeners is a > crucial part of most browser scripting applications. Do it wrong and > your application will likely be crippled and any end-user will tell > you a crippled application is worse than one that doesn't work at all. > > The first question is what type of object(s) will need listeners > attached. Windows? Documents? Elements? All of these? This is not a > question to be taken lightly as attaching a listener to the wrong > object will often fail silently (or work in one browser but not the > next). > > There isn't any practical way to cram all three of these scenarios > into one function. For one, there is no standard way to determine one > type of host object from another. That should shoot down the idea > right there. You will need at least one function per host object type. > > Let's start with windows. The first thing you need to know is that > window objects were not part of the standard recommendations used by > the authors of many (if not most) browsers in use today. It doesn't > really matter if HTML5 "standardizes" the window object now as HTML5 > exists only on paper. Perhaps in five years HTML5's recommendations > will be relevant to consider. For now, save for its history, the > window object is one big unknown. > > // Degrades in IE 8- > // Also degrades in some older browsers that lack this method on > window objects > // No frames > > // NOTE: Abbreviated for examples -- use isHostMethod to detect host > methods > > if (window.addEventListener) { > var attachWindowListener = function(eventType, fn) { > window.addEventListener(eventType, fn, false); > }; > } > > Now the question is what sort of events would history and common sense > tell you belong to the window? These four come to mind:- > > - load > - scroll > - resize > - orientationchange > > You should never assume that other DOM events (e.g. click, mousedown) > will bubble up to the window object because there is no standard > recommendation that says they have to. > > if (window.addEventListener) { > var attachWindowListener = function(eventType, fn) { > > // Remove this line on deployment -- for debugging only > if (!(/^(load|scroll|resize|orientationchange)$/.test(eventType))) > { > throw new Error('Use attachListener with an element.'); > } > > window.addEventListener(eventType, fn, false); > }; > } > > Will all four of those work on the window object? Certainly can't say > for sure. ISTM that there has been at least one browser in the last > decade that required "scroll" listeners to be attached to the > document. So you would rarely use this directly, but through a wrapper > that attaches multiple listener types (e.g. attachReadyListener from > previous edition). > > For another example, an attachOrientationListeners wrapper would > attach both "orientationchange" and "resize" listeners (and then deal > with only one type, determined the first time through). This is all > you can do as there is no reliable way to detect what sort of event > listeners will fire. > > You can detect which DOM0 event handler attributes (e.g. > "ontouchstart") are supported (as seen in a dozens of variations of my > original test), but that is only for use with elements and doesn't > give you enough information to decide which types of listeners your > script should attach (unless it is using setAttribute to create event > handler attributes!) > > As noted, the above renditions degrade in IE 8-. In some contexts, > that's perfect. In others, it's a show-stopper. You should be able to > create a forked rendition that uses attachEvent at this point. If not, > see my listener examples on JSPerf (link at end). > > What about documents? The middle child is neglected; other than > DOMContentLoaded, there's typically nothing to do and therefore no > wrappers are needed. > > That leaves elements:- > > // Degrades in IE 8- > // Also degrades in some older browsers that lack this method on > window objects > // No frames > > // NOTE: Abbreviated for examples -- use isHostMethod to detect host > methods > > if (document.documentElement && > document.documentElement.addEventListener) { > var attachListener = function(el, eventType, fn) { > el.addEventListener(eventType, fn, false); > }; > > var detachListener = function(el, eventType, fn) { > el.removeEventListener(eventType, fn, false); > }; > } > > How about "binding" the - this - object for the listener? > > if (attachListener && Function.prototype.call) { > var bindListener = function(el, eventType, fn, thisObject) { > > var listener = function(e) { > fn.call(thisObject || el, e); > }; > > attachListener(el, eventType, listener); > > // NOTE: Returns bound listener function > > return listener; > }; > > var unbindListener = function(el, eventType, boundListener) { > detachListener(el, eventType, boundListener); > }; > } > > Why include the seemingly redundant "unbindListener"? To reinforce > that it expects a previously bound listener, not the original > function. > > That should close the book on "event registries". There's no point in > reproducing that functionality; just let the browser handle it. You > say you don't want to "keep track" of the returned function reference? > Just put it where you wherever you were going to track the original > function (you don't need it anymore). > > What about delegation? The first thing you need is the event target:- > > // Will not work in IE 8-, so use only with attachListener renditions > that degrade in IE 8- > > function getEventTarget(e) { > var target = e.target; > > // Check if not an element (e.g. a text node) > > if (1 != target.nodeType) { > > // Set reference to parent node (which must be an element) > > target = target.parentNode; > } > return target; > } > > ...Or if you will be using a forked rendition to support IE 8-:- > > function getEventTarget(e) { > var target = e.target; > > if (target) { > > // Check if not an element (e.g. a text node) > > if (1 != target.nodeType) { > > // Set reference to parent node (which must be an element) > > target = target.parentNode; > } > } else { > target = e.srcElement; > } > return target; > } > > You may also want the "related target" for certain pointer-related > events:- > > function getRelatedEventTarget(e) { > var target = e.relatedTarget; > > // Check if not an element (e.g. a text node) > > if (1 != target.nodeType) { > > // Set reference to parent node (which must be an element) > > target = target.parentNode; > } > return target; > } > > ...but you will have to wait for the book (or dig through My Library) > for the IE rendition of that one. > > Once you have the target, the standard procedure is to find an > ancestor matching a predetermined set of criteria (e.g. tag name, > class name, etc.) and use it for the - this - object when calling the > "listener" (really just an unattached callback function). > > The signature looks like this:- > > var delegateListener = function(el, eventType, fn, fnDelegate) { > // ... > } > > Instead of setting of using a single reference for - this - for all > callbacks, a reference is supplied dynamically by the delegation > callback function. Will leave that as exercise. Note that it will have > to return a function, as in the "bindListener" function. > > Also note that you could create a wrapper for this function called > "delegateListenerByQuery" and have the callback function run a query > to find the appropriate ancestor. Like most schemes involving queries, > this is not recommended at all past the mock-up stage. Event handling > needs to be done as simply and quickly as possible, which lets queries > out. Besides, there are no ancestor-based queries (except in a newer > version of MooTools I think). :) I think it's safe to say that most of > the time you will find the ancestor in fewer hops going up from the > element, rather than down from the document. YMMV. > > You might be tempted to "bundle" all of these (four!) arguments in a > single options object. Don't. It's just not a good idea at this > (foundation) level. These functions get called often at run time, but > are often wrapped in higher-level functions, so the application > developer is not often typing out the signatures. Creating and > discarding an object on every function call is just a self-imposed > performance penalty. Granted, that technique can make for some really > "cool" looking code. :) > > How about preventing the default action? > > // NOTE: Does not work in IE 8-, so use only with attachListener > renditions that degrade in IE 8- > > function cancelDefault(e) { > e.preventDefault(); > } > > For IE 8-:- > > function cancelDefault(e) { > e.returnValue = false; > } > > These are created as companion renditions to the attach/detachListener > functions. If using the forked rendition, put each in the appropriate > "prong". > > And that's about it, isn't it? You want to cancel bubbling. No, you > just think you want to do that. It's never a good idea as your widget > (or whatever) does not own the event. Other listeners may very well be > interested in what it has to say. > > And speaking of bubbling, not all events can be expected to bubble. > These come to mind as suspect:- > > - focus > - blur > - change > - submit > - reset > > As for the last two, you'd have to have a pretty crazy design to want > to delegate those. If you have such a design, you need to change it as > there is not a chance in hell in "normalizing" those reliably. You'd > really have to be touched to try. > > As for the change event, delegation is highly context-sensitive, > particularly in deciding just when a SELECT should indicate it has > changed. There's three common scenarios and a whole chapter in the > book on that. > > It can be useful to delegate focus/blur (e.g. for instant validation > feedback). Though there is not a chance of "normalizing" those either. > As always, you have to think about what you are going to do with the > functions. Once you have the basic focus/blur delegation, you have the > foundation for the most basic change rendition. > > Of course, you may never need any of these higher-level wrappers as > you may not need for focus/blur/etc. events to bubble. The one thing > you don't need is to start tangling up the basic attach/detach logic > with "normalization" code. > > If using XHR (asynchronous, of course), SQL, Geo Location or anything > else that calls back asynchronously, in conjunction with DOM events, a > common message queue can make it easier to manage the interactions > between the user, their local storage, the server and the document. > You don't need AOR or "custom events", just a simple structure to > queue up and deliver messages to "subscribers". This is how widgets > talk to applications, frameworks and other widgets. > > What to expect from the "standard" libraries (e.g. jQuery). What else? > One pair of methods for all types of host objects, complicated event > registries, (in the case of jQuery) no way to set the - this - object > without calling an obscure (and highly amusing) "proxy" method, logic > tangled up in queries (e.g. "Live"), etc. They also tangle up their > basic functions with all manner of confused "normalization" logic, > further muddying the context in which these scripts can be expected to > "work". This is unfortunate as they typically have no fallback plan. > If browsers don't meet *their* expectations, they are just not > responsible for what their script may do to *your* document. You saw > the browser icons. As for the end-users, they are on their own; > anything goes. > > And if you are lucky, they may even throw in some browser sniffing. So > you've got that to look forward to. :) > > At the very least, you'd expect the libraries to fix the Internet > Explorer Memory Leak Problem. But such expectations are dashed as most > (including jQuery last I checked) create leaks and then try to "fix" > them in an unload listener. Yes, they don't avoid creating leaks, but > just accept that leaks are a given and tack on an unload listener to > "clean up" on navigation. Thanks Crockford. Love the CSS Resets too! : > ( > > But what if your site is one of those single-page applications that > never navigates? It will continue to leak as you add listeners and, if > it runs long enough, will eventually crash the browser (or at least > the tab). > > Hopefully, if you have been paying attention over the years, you > avoided turning your site into a single-page wonder. The entire > chapter on managing browser history reads: don't even think about it > (you idiot). > > http://www.cinsoft.net/ > http://www.twitter.com/cinsoft > http://jsperf.com/browse/david-mark