Unbound Listeners
Table of Contents
The Setup
One of the trickiest things to get right in javascript is this
. To illustrate this point, let’s look at a situation we came across with one of my favorite browser APIs: addEventListener
.
Attaching a load
event listener to the window
object is pretty straight forward:
|
|
People start to get themselves into trouble when they assign references to instance methods and later want to execute those instance methods. Too frequently, I see code that looks like:
|
|
this
, as seen from inside callback()
, should be a reference to the window
object. And that works, mostly - but only because you are getting lucky. The assert
passes, but only because this
has to be something:
The moment you assign a reference to an instance method to a local variable, you’ve essentially unbound the method. And when invoked, you’ve supplied an undefined
this
value to the context of the callback. this
will fall back to it’s original value - window
- which is why the assert
passes.
These two patterns show the same issue, and might be more obviously problematic:
|
|
|
|
You can make this work by using call
(or apply
or bind
), but it’s fairly pathological:
|
|
So, why is this a problem? Well, because we can’t guarantee that the callback executes in the top window
context.
Enter third-party analytics
Most third party analytics libraries instrument the EventTarget.prototype.addEventListener
method to give themselves greater insight into stack traces and also to measure various aspects of performance. My favorite third party analytics library, boomerang, prefers to load itself into a same-origin child IFRAME of the base page so as to be non-blocking in a legacy-browser-friendly way (more on this soon!). That means that our instrumentation code needs to run on top.EventTarget
, so it looks something like this:
|
|
And now, you might see the problem that exists if the window.addEventListener
method is unbound from window
:
- the
this
in theinstrument()
method above will resort to being a reference to thewindow
of the IFRAME - which means listeners will be bound to the wrong window
load
events will fire with regard to the inner IFRAME, not the topwindow
- you won’t get the
message
events that werepostMessage
ed to the topwindow
- for the events you do get, because the
this
in theintercede
method is the child IFRAME, they too will have the wrong context - potentially tricking up developer code
We saw this so frequently that we now defensively work around it in instrument()
- built on the presumption that no one actually intends to bind a listener to the boomerang child IFRAME:
|
|
Two libraries conspiring to unbind all window
listeners
On numerous occasions, we have been entangled with a particular polyfill library (thousands of downloads per week from npm) that patches addEventListener
in a way such that, when coexisting in the page with boomerang, user code cannot not bind listeners to the wrong window. The code of the polyfill library was basically:
|
|
This essentially assigns an unbound reference to the bound window.addEventListener
method. Even if you explicitly call window.addEventListener(...)
, that just ends up invoking the unbound method.
Like I showed, we are now working around this from our end - and we also patched up the polyfill library with a PR - but for months, when boomerang and this polyfill library coexisted on a site, users were unable to bind listeners to the top window
. 😢
With your console open, check out the broken interaction here, fixed interaction here.
The Lesson
Don’t assign or pass around references to methods that are context-aware because you can’t guarantee that they will be given the correct context when they are ultimately executed.