Javascript object destroy event handler - javascript

MyMapStaticObject
var PlaceViewModel = function(){
MyMapStaticObject.addLayer(someLayer);
}
PlaceViewModel.prototype.addMarker = function(item){
}
I have a PlaceViewModel that has a function named addMarker to add marker to map. I will use PlaceViewModel new istances in different classes.
var inst = new PlaceViewModel();
When I initialize the PlaceViewModel, I am adding new layer to map via MyMapStaticObject. I should remove layer when instance destroyed.
Can I handle javascript destroy event?

Javascript does not have a destroy event. It is a garbage collected language and will free an object when there is no longer any code that can reach the object reference. When it does free the object, it does not provide any event to notify of that.
If you want to implement some sort of clean-up code that will remove the layer, then you will have to add a method that you can call when you are done with the object, so you can call that method and it can then remove the layer in that method. Calling this method will have to be a manual operation on your part (most likely it will be hooked into the management of other things going on in your code and you can call it by that code at the appropriate time).

Bit late to the party here, but I wanted to know when an object got destroyed. Problem is JS doesn't have any built in way of doing this. I wanted this so that I could add websocket events to a page, and then remove when it went to another page, of course I could have implemented this in say the page loading section, but I have different frameworks and wanted a more generic solution to object destroying problem. Javascript is a garbage collected language, but it still would have been nice to have some life-cycle events we could attach too. There is of course proxy's, but again that wouldn't help here, because it's the proxy itself I would need to know that has been deleted.
Well, there is one place you do get a kind of destroy event, and that's with the MutationObserver, that most modern browsers now support. It's not strictly destroying, it's adding and removing nodes from the DOM. But generally speaking if you have events, you are likely to have some DOM node you could attach too, or even if it's none visual you could just add a none visible DOM element.
So I have a little function called domDestroy(element, obj), when it detects the element been removed, it then checks if the obj has a destroy method, if one exists it will call it.
Now one gotcha I had is that I create my pages in an hidden DOM node, and of course when I placed into the visible DOM node, I was getting a delete because I was detaching from the invisible DOM node, and then attaching to the visible DOM. Not what we want at all.
The solution was pretty simple, when doing this kind of double buffering, it's normally done in 1 step, eg. hide current page, show new page. So what I do is keep track of when it's been removed and keep in a simple Set, and then also keep track of elements been added, and if this element is part of the Set I will remove it. I then just check this Set again on the next tick, if's it's still there, it's been really deleted and we call the destroy method of the object.
Below is a simple example, basically if you right click and inspect the page, you can move the LI's up and down with dragging and dropping, this would cause a DOM detach and re-attach,. But if you instead delete one of the LI's, you will notice it say delete then, because it now knows it wasn't re-attached to another DOM.
Of course, one thing to be aware of, if you do any attaching / detaching of DOM elements try and do this within the same tick, IOW: be aware of asynchronous ops in between. Also you might use detached DOM's to build your pages, here you could easily alter the function to cope with this too, basically add these using the destroyObserver.observe(.
const dsy = "__dom-destroy-obj";
const destroyList = new Set();
let tm;
function cleanUp() {
tm = null;
for (const el of destroyList) {
for (const d of el[dsy]) {
d.destroy();
}
}
destroyList.clear();
}
function checkDestroy(el) {
if (el[dsy]) {
for (const d of el[dsy]) {
if (!d.destroy) {
console.warn("No destroy, on dom-destroy-obj target");
} else {
destroyList.add(el);
if (tm) return; //already a timer running
tm = setTimeout(cleanUp, 1);
}
}
}
if (el.childNodes) for (const n of el.childNodes) checkDestroy(n);
}
function checkAdded(el) {
if (el[dsy]) {
destroyList.delete(el);
}
if (el.childNodes) for (const n of el.childNodes) checkAdded(n);
}
const destroyObserver = new MutationObserver(
function (mutations) {
for (const m of mutations) {
if (m.removedNodes.length) {
for (const i of m.removedNodes) {
checkDestroy(i);
}
}
if (m.addedNodes.length) {
for (const i of m.addedNodes) {
checkAdded(i);
}
}
}
}
);
destroyObserver.observe(document.body, {
childList: true,
subtree: true
});
function domDestroy(element, obj) {
if (!element[dsy]) element[dsy] = new Set();
element[dsy].add(obj);
}
//simple test.
for (const i of document.querySelectorAll("li")) {
domDestroy(i, {
destroy: () => console.log("destroy")
});
}
<span>
From your browsers inspector, try moving the LI's, and deleting them. Only when you delete the DOM node, should the destroy method get called.
</span>
<ul>
<li>Re order</li>
<li>Or delete</li>
<li>Some of these</li>
</ul>

Related

How to Get Event Listener of an Element

Is there any way to get the list of all event listeners of an element on the HTML page using JavaScript on that page.
Note: I know we can see them with Chrome dev tools event listeners but I want to log/access see list using the JavaScript of the page.
Also, I know we can get them through jQuery but for that, we also have to apply the events using jQuery, but I want something that would be generic so I could also access the event listeners applied to other elements such as web components or react components.
If you really had to, a general way to do this would be to patch EventTarget.prototype.addEventListener:
const listeners = [];
const orig = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(...args) {
if (this instanceof HTMLElement) {
listeners.push({
type: args[0],
fn: args[1],
target: this,
});
}
return orig.apply(this, args);
};
document.body.addEventListener('click', () => console.log('body clicked'));
console.log(listeners[0].fn);
click this body
To find listeners attached to an element, iterate through the listeners array and look for targets which match the element you're looking for.
To be complete, also patch removeEventListener so that items can be removed from the array when removed.
If you need to watch for listeners attached via on, then you'll have to do something similar to the above to patch the HTMLElement.prototype.onclick getter/setter, and for each listener you want to be able to detect.
That said, although you said you want a generic solution, rather than patching built-in prototypes, it'd be better to add the listeners through jQuery or through your own function.
What I did when I had a similar problem is add a data attribute when the listener was set, so I could identify it later.
At the end of the function that adds the listener:
elm.setAttribute('data-has_mask', true);
At the beginning of that same function:
if("true" == elm.getAttribute('data-has_mask')) {
return;
}
Maybe not exactly what the OP is looking for, but I was having a lot of trouble with this, and this is an obvious solution for a particular use case, and I guess it might help someone out.

What are the differences between setting DOM attribute directly and via requestAnimationFrame?

I am reading BluePrintJS source code and noticed they put DOM element attributes changing logic inside a requestAnimationFrame block. What are the differences to set DOM attribute directly and via requestAnimationFrame?
private handlePopoverClosing = (node: HTMLElement) => {
// restore focus to saved element.
// timeout allows popover to begin closing and remove focus handlers beforehand.
requestAnimationFrame(() => {
if (this.previousFocusedElement !== undefined) {
this.previousFocusedElement.focus();
this.previousFocusedElement = undefined;
}
});
const { popoverProps = {} } = this.props;
Utils.safeInvoke(popoverProps.onClosing, node);
};
Performance. requestAnimationFrame will be run when at the start of a render frame, which helps with something known as layout thrashing.
By doing this you group all DOM changes into one frame which is done all at once rather than spreading rendering logic across multiple frames, which is more expensive (DOM work is s l o w).
The general idea is that you want to group DOM writes such that they happen before DOM reads instead of interspersing reads among writes - rAF does that by ensuring the write happens at a specific time, with other writes.

How to change the HTML content as it's loading on the page

I do A/B Testing on our site and I do most of my work is in a JS file that is loaded at the top of the page before anything else is rendered but after jQuery has loaded which comes in handy at times.
Taking a very simple example of changing an H1 tag, I would normally inject a style in the head to set the H1 opacity to 0 and then on DOMContentLoaded, I would manipulate the H1 contents and then set the opacity to 1. The reason for this is to avoid a flash of the old content before the change takes place - hiding the whole object is more graceful on the eye.
I've started to look at the MutationObserver API. I've used this before when changing content in an overlay dialog box that the user could open which seems to be quite a cool approach and I'm wondering if anyone has managed to use a MutationObserver to listen to the document as it's first loading/ parsing and make changes to the document before first render and before DOMContentLoaded?
This approach would then let me change the H1 content without having to hide it, change it, then show it.
I've attempted but failed so far and have just ended up reading about the to-be-obselete Mutation Events and wondering if I'm trying to do something that just isn't possible. However we've (not me) have managed to put a robot on Mars so I'm hoping I can solve this.
So is it possible to use MutationObservers to change the HTML content on-the-fly as the page is being loaded/ parsed?
Thanks for any help or any pointers.
Regards,
Nick
The docs on MDN have a generic incomplete example and don't showcase the common pitfalls.
Mutation summary library provides a human-friendly wrapper, but like all wrappers it adds overhead.
See Performance of MutationObserver to detect nodes in entire DOM.
Create and start the observer.
Let's use a recursive document-wide MutationObserver that reports all added/removed nodes.
var observer = new MutationObserver(onMutation);
observer.observe(document, {
childList: true, // report added/removed nodes
subtree: true, // observe any descendant elements
});
Naive enumeration of added nodes.
Slows down loading of enormously big/complex pages, see Performance.
Sometimes misses the H1 elements coalesced in parent container, see the next section.
function onMutation(mutations) {
mutations.forEach(mutation, m => {
[...m.addedNodes]
.filter(node =>
node.localName === 'h1' && /foo/.test(node.textContent))
.forEach(h1 => {
h1.innerHTML = h1.innerHTML.replace(/foo/, 'bar');
});
});
}
Efficient enumeration of added nodes.
Now the hard part. Nodes in a mutation record may be containers while a page is being loaded (like the entire site header block with all its elements reported as just one added node): the specification doesn't require each added node to be listed individually, so we'll have to look inside each element using querySelectorAll (extremely slow) or getElementsByTagName (extremely fast).
function onMutation(mutations) {
for (var i = 0, len = mutations.length; i < len; i++) {
var added = mutations[i].addedNodes;
for (var j = 0, node; (node = added[j]); j++) {
if (node.localName === 'h1') {
if (/foo/.test(node.textContent)) {
replaceText(node);
}
} else if (node.firstElementChild) {
for (const h1 of node.getElementsByTagName('h1')) {
if (/foo/.test(h1.textContent)) {
replaceText(h1);
}
}
}
}
}
}
function replaceText(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
for (let node; (node = walker.nextNode());) {
const text = node.nodeValue;
const newText = text.replace(/foo/, 'bar');
if (text !== newText) {
node.nodeValue = newText;
}
}
}
Why the two ugly vanilla for loops? Because forEach and filter and ES2015 for (val of array) could be very slow in some browsers, see Performance of MutationObserver to detect nodes in entire DOM.
Why the TreeWalker? To preserve any event listeners attached to sub-elements. To change only the Text nodes: they don't have child nodes, and changing them doesn't trigger a new mutation because we've used childList: true, not characterData: true.
Processing relatively rare elements via live HTMLCollection without enumerating mutations.
So we look for an element that is supposed to be used rarely like H1 tag, or IFRAME, etc. In this case we can simplify and speed up the observer callback with an automatically updated HTMLCollection returned by getElementsByTagName.
const h1s = document.getElementsByTagName('h1');
function onMutation(mutations) {
if (mutations.length === 1) {
// optimize the most frequent scenario: one element is added/removed
const added = mutations[0].addedNodes[0];
if (!added || (added.localName !== 'h1' && !added.firstElementChild)) {
// so nothing was added or non-H1 with no child elements
return;
}
}
// H1 is supposed to be used rarely so there'll be just a few elements
for (var i = 0, h1; (h1 = h1s[i]); i++) {
if (/foo/.test(h1.textContent)) {
// reusing replaceText from the above fragment of code
replaceText(h1);
}
}
}
I do A/B testing for a living and I use MutationObservers fairly often with good results, but far more often I just do long polling which is actually what most of the 3rd party platforms do under the hood when you use their WYSIWYG (or sometimes even their code editors). A 50 millisecond loop shouldn't slow down the page or cause FOUC.
I generally use a simple pattern like:
var poller = setInterval(function(){
if(document.querySelector('#question-header') !== null) {
clearInterval(poller);
//Do something
}
}, 50);
You can get any DOM element using a sizzle selector like you might in jQuery with document.querySelector, which is sometimes the only thing you need a library for anyway.
In fact we do this so often at my job that we have a build process and a module library which includes a function called When which does exactly what you're looking for. That particular function checks for jQuery as well as the element, but it would be trivial to modify the library not to rely on jQuery (we rely on jQuery since it's on most of our client's sites and we use it for lots of stuff).
Speaking of 3rd party testing platforms and javascript libraries, depending on the implementation a lot of the platforms out there (like Optimizely, Qubit, and I think Monetate) bundle a version of jQuery (sometime trimmed down) which is available immediately when executing your code, so that's something to look into if you're using a 3rd party platform.

How do I dispose of a prototype in JS?

I've got a thorny issue and although the answer may be obvious, I cannot see how to do what I'm trying to do.
I have created a script library for my application that uses JS prototypes and templating to dynamically instantiate DOM elements AND to wire those elements up with handlers. An example is the following:
var ppane = new AWP.iuiPanel(theObject, { title: 'Select filter(s)', idDisplay: 'block', idString: params.sender._options['title'] });
AWP.iuiPanel is a class defined as a function prototype, e.g:
AWP.iuiPanel = function() { <i'm a constructor> }
AWP.iuiPanel.prototype = { <a bunch of methods here> }
The methods inside the instance create a DOM element (in this case a floating panel) and establish event bindings for it, wire up its control elements, etc.
The advantage of going down this path is that through a single call to create a new instance of a class I can also build the associated DOM element, and once instantiated, the class methods that have been wired up will execute against the element to do things like position it relative to a target object, respond to relevant browser events, etc.
The problem I have is when I want to dispose of this construct. I can dispose the DOM element easily. But I then still have the class instance in memory with methods wired to browser events looking for the DOM element that has been disposed. I need to be able to dispose not only of the DOM element, but also of the class instance, and I cannot figure out how to do that.
How can one dispose of a function prototype once declared? This seems like it ought to be simple, but I'm finding it to be decidedly not so.
For background info, here is an example of a class as I am defining it:
This is necessarily pseudo-code(ish)...
AWP.trivialExample = function(someDomRef, someOptionSet) {
this._id = someOptionSet['name'];
this._width = someOptionSet['width'];
this._width = someOptionSet['height'];
this._domRef = someDomRef;
this._object = '';
this.constructDOM();
this.wireEvents();
}
AWP.trivialExample.prototype = {
constructDOM: function() {
// build a complex DOM element relative to a provided DOM ref using the
// desired and height. This uses a template and I won't give a precise example
// of such a template.
jQuery("#aTemplate").tmpl(someJSONData).appendTo("body");
},
positionRelative: function() {
// this function would get the location of a specific DOM ref and always maintain
// a relative position for the DOM element we just constructed
},
wireEvents: function() {
// hook up to events using JQuery (example)
jjQuery(window).resize(this.positionRelative);
}
}
The above is a trivial example that would take in a DOM object reference, and then it would dynamically construct a new DOM element and it would wire up to browser events to always maintain relative position between these two objects when the page is sized.
When I dispose of the new object, I also need to dispose of the class instance and I cannot find a simple way to do that.
All help appreciated.
Thanks;
A suggestion on the event listeners referencing a deleted DOM node:
just as you have a 'wireEvents', you should have a corresponding 'unwireEvents' in case you decide to stop using the object. addEventListener() needs to be used in conjuction with removeEventListener() in this case. You should modify your prototype to remove Event listeners when the corresponding DOM Node is 'disposed', as you say.
AWP.iuiPanel.prototype = null; // ?

Sending data with an event listener

I have a predicament: I want to send some data with an event listener but also be able to remove the listener. Here's the standard closure approach...
var fn = function(e){method(e,data)};
el.addEventListener('click',fn,false);
el.removeEventListener('click',fn,false);
and you could remove the event, just fine. But say, the element was removed from the DOM? Then, you'd be left with the fn function sitting around. After removing a couple thousand DOM elements, it will result in something of a memory leak.
I've considered attaching a DOMNodeRemoved event handler, that would remove any left over functions/data along with the removed node. But apparently, that event isn't cross-browser compatible.
The only other option I've come up with would be modifying the element's DOM. Consider...
el.MyEventData = function(e){method(e,data)};
el.addEventListener('click',el.MyEventData,false);
el.removeEventListener('click',el.MyEventData,false);
Is modifying the DOM acceptable in this situation? The only sticky part of that solution is when you try to add more than one event listener. Let's say we made a custom function to parse the adding/removing of events...
function makeEvent(fn,data){
var dataFn = function(e){fn(e,data)};
//create object to hold all added events
el.myEvents = {};
//make ID for this specific event
var eventID = ranString();
//add the event to the events object
el.myEvents[eventID] = [fn,dataFn];
//finally add the listener
el.addEventListener('click',dataFn,false);
}
function destroyEvent(fn){
//find all fn references
for(var id in el.myEvents){
if (el.myEvents[id][0] == fn){
el.removeEventListener('click',el.myEvents[id][1],false);
el.myEvents[id] = null;
}
}
}
It still modifies the DOM, as before, and certainly isn't a very elegant solution either. Does anyone know of any alternative, better method for passing data?
EDIT: So, I've looked into a little of jQuery's data/event scripts. I don't completely understand the code, so if someone would clarify, it would be helpful. But it seems as though they use a similar method, by making some type of el.cache property, that holds event data.
Considering that you use addEventListener this is not an issue as all modern garbage collectors can take care of such situations. The problem with event listeners only exists in IE's implementation (7-).
Test - 10 000 addEventListener and remove element (see Windows Task Manager)
When a DOM object contains a reference
to a JavaScript object (such an event
handling function), and when that
JavaScript object contains a reference
to that DOM object, then a cyclic
structure is formed. This is not in
itself a problem. At such time as
there are no other references to the
DOM object and the event handler, then
the garbage collector (an automatic
memory resource manager) will reclaim
them both, allowing their space to be
reallocated. The JavaScript garbage
collector understands about cycles and
is not confused by them.
http://www.crockford.com/javascript/memory/leak.html
Did you consider .delegate()?
According to your jQuery question:
Each jQ object has a data property. It does not stored inside the element itself - it's very important. jQ use general storage for all elements - jQuery.cache. So when you add anything to the element like this:
$('#myEl').data('someValue', 1);
jQ do the following:
jQuery.cache[elementUniqId]['someValue'] = 1;
So element does not contain its data object. It only have an uniq id that is allows it to access to the data recorde at the global storage. (elementUniqId is autogenerated)
jQ events are stored into the element data as well:
$('#myEl').click(function() { first listener });
$('#myEl').mouseenter(function() { one more listener });
$('#myEl').click(function() { anotheer listener });
Will be stored:
jQuery.cache[elementUniqId]['events'] = {
click: [function() { first listener }, function() { anotheer listene }],
mouseenter: [function() { one more listener }]
};
It allows jQ to store the order of execution for all listeners attached to each event. And later, when you delete dom element, using jQuery - .remove(), jQuery loops through the jQuery.cache[elementUniqId]['events'] and remove each listener from the element, and after removes element cache record. It allows jQ to preven memory leaks
A possible solution to maybe take you in a different direction: add the function as an inline sibling of the element.
<span id='element12345'>Test</span><script
type='text/javascript'>function fn12345() { /* ... */ }</script>
Then, when you remove all the event listeners that you want, you can also remove the "nextSibling()" of the element you're working with.
how about a setup like this? (using IE syntax since that's what I have available right now)
<div id="clickbox" style="width: 100px; height: 100px; border: 1px solid orange;">
click here to test</div>
<input id="Button1" type="button" value="clear handler" />
<script>
var data = "derp1";
var el = document.getElementById('clickbox');
var btn = document.getElementById('Button1');
// methods
var method = function (e, dat2) { alert(dat2); };
var fn = function (e) { method(e, data) };
var remover = null;
// attachment
el.attachEvent('onclick', fn, false);
(function (id, handler) {
// handler variable is local but points to the right function
remover = function (e) {
if (document.getElementById(id)) {
// remove the original listener (handler is fn)
document.getElementById(id).detachEvent('onclick', handler, false);
alert('detached');
}
// remove last reference to the original method
handler = null;
alert('method nulled');
// clean up the remover method
e.srcElement.detachEvent('onclick', remover);
remover = null;
};
btn.attachEvent('onclick', remover);
})('clickbox', fn);
// clear the original variable but the method still exists as an event listener
fn = null;
// you should be able to remove the div element and any references to it
// without leaving any stray bits around.
setTimeout( function() {
var d = document.getElementById('clickbox');
if (d){ d.parentNode.removeChild(d) ; }
} , 6000 );
el = null;
btn = null;
</script>
I'm assuming you don't want the listener removed immediately after adding it but rather want to be able to remove it at a later time. to deal with this, the cleanup routine is given its own scope by creating an anonymous function which is immediately invoked with fn as a parameter. the anon function then has its own reference to fn maintained in the handler variable. after that, fn can be cleaned up and the remaining references to the original method exist in the listener list for your element and in the scope of the anonymous function.
within the anonymous function scope, the function remover has access to the handler variable and can use it to detach the listener. remover then detaches and clears itself so there should be nothing left with access to any version of fn/handler.
I don't have any way to verify all this right now but I think it makes sense and should hold up in modern browsers.
why not take a look at this
Binding Events To Non-DOM Objects With jQuery
http://www.bennadel.com/blog/1520-Binding-Events-To-Non-DOM-Objects-With-jQuery.htm

Categories