Subsequent to removeEventListener in bootstrapped addon not working when addon disabled, I am exploring other possibilities.
Beside using bind()
and cac
Since we're talking restartless add-ons... A lot of restartless add-ons use unload
and unloadWindow
helper functions, to make it easier to implement shutdown
properly and also help with stuff like addEventListener
, so bear with me for a bit.
unload
First, unload
is a helper function that you pass another function to, that will be run upon shutdown
(or can be called manually). Most implementations are extremely similar to this:
var unloaders = []; // Keeps track of unloader functions.
function unload(fn) {
if (typeof(fn) != "function") {
throw new Error("unloader is not a function");
}
unloaders.push(fn);
return function() {
try {
fn();
}
catch (ex) {
Cu.reportError("unloader threw " + fn.toSource());
Cu.reportError(ex);
}
unloaders = unloaders.filter(function(c) { return c != fn; });
};
}
You'd then hook up shutdown
to do the right thing:
function shutdown() {
...
for (let i = unloaders.length - 1; i >= 0; --i) {
try {
unloaders[i]();
}
catch (ex) {
Cu.reportError("unloader threw on shutdown " + fn.toSource());
Cu.reportError(ex);
}
}
unloaders.length = 0;
}
unload
Now you can do stuff like:
function startup() {
setupSomething();
unload(removeSomething);
setupSomethingElse();
var manualRemove = unload(removeSomethingElse);
...
if (condition) {
manualRemove();
}
}
unloadWindow
You'll usually want to create a second function unloadWindow
to unload stuff when either your add-on is shut down or the window gets closed, whatever happens first. Not removing stuff when the window gets closed can be very tricky, and create Zombie compartments of your bootstrap.js
and/or code modules very easily (this is from experience writing and reviewing restartless add-ons).
function unloadWindow(window, fn) {
let handler = unload(function() {
window.removeEventListener('unload', handler, false);
try {
fn();
}
catch (ex) {
Cu.reportError("window unloader threw " + fn.toSource());
Cu.reportError(ex);
}
});
window.addEventListener('unload', handler, false);
};
(Some people might want to "optimize" this, as to have only one "unload"
handler, but usually you only have so unloadWindow
calls that it won't matter.)
Now you can .bind
stuff and do whatever and let the the unloader closures keep track of it. Also, you can use this to keep your shut down code next to your initialization code, which might increase readability.
function setupWindow(window, document) {
var bound = this.contextPopupShowing.bind(this);
contextMenu.addEventListener('popupshowing', bound, false);
unloadWindow(window, function() {
contextMenu.removeEventListener('popupshowing', bound, false);
});
// Or stuff like
var element = document.createElement(...);
contextMenu.appendChild(element);
unloadWindow(window, function() {
contextMenu.removeChild(element);
});
// Or just combine the above into a single unloader
unloadWindow(window, function() {
contextMenu.removeEventListener('popupshowing', bound, false);
contextMenu.removeChild(element);
});
}
http://2ality.com/2013/06/auto-binding.html
var listener = myWidget.handleClick.bind(myWidget);
domElement.addEventListener('click', listener);
...
domElement.removeEventListener(listener);
You don't have to use bind
for addEventListener
. You can use handleEvent
. It was in that topic I linked you too:
Removing event listener which was added with bind
MDN :: EventTarget.addEventListener - The value of "this" within the handler
handleEvent
is actually a common way the javascript code in the firefox codebase does it.
Copied straight from MDN:
var Something = function(element) {
this.name = 'Something Good';
this.handleEvent = function(event) {
console.log(this.name); // 'Something Good', as this is the Something object
switch(event.type) {
case 'click':
// some code here...
break;
case 'dblclick':
// some code here...
break;
}
};
// Note that the listeners in this case are this, not this.handleEvent
element.addEventListener('click', this, false);
element.addEventListener('dblclick', this, false);
// You can properly remove the listners
element.removeEventListener('click', this, false);
element.removeEventListener('dblclick', this, false);
}
Where I mostly use bind
is when doing a for
loop and I make anonymous functions with something in the array like arr[i]
. If I don't bind it then it always just takes the last element of the array, I have no idea why, and I hate it, so then I go to using [].forEach.call(arr, function(arrI)
.
Before bind()
was supported you had to save a reference to this
outside the function. Then pass in a function that can forward the invocation the way you want.
var self = this;
contextMenu.addEventListener('popupshowing', function() {
self.contextPopupShowing.apply(self, arguments);
}, false);
In this case we use apply
to set the context to self
, our saved version of this
, and to send it whatever arguments
were passed to the anonymous function via the magic arguments
keyword that contains the list of arguments that a function was passed when invoked.