2

I want to have literally a Dictionary<Node, Object>

This is basically an ES6 WeakMap but I need to work with IE8.

The main feature I want is

  • minimize memory leaks
  • O(1) lookup on Object given Node.

My implementation:

var uuid = 0,
    domShimString = "__domShim__";

var dataManager = {
    _stores: {},
    getStore: function _getStore(el) {
        var id = el[domShimString];
        if (id === undefined) {
            return this._createStore(el);
        }
        return this._stores[domShimString + id];
    },
    _createStore: function _createStore(el) {
        var store = {};
        this._stores[domShimString + uuid] = store;
        el[domShimString] = uuid;
        uuid++;
        return store;
    }
};

My implementation is O(1) but has memory leaks.

What's the correct way to implement this to minimize memory leaks?

Raynos
  • 166,823
  • 56
  • 351
  • 396
  • So you want to implement something like `jQuery.cache`? So are you worried about the uuid on the element, or about orphaned data? – RightSaidFred Dec 01 '11 at 19:07
  • @RightSaidFred mainly the orphaned data. Once the element goes out of scope that data object doesn't dissappear. – Raynos Dec 01 '11 at 19:11
  • Is that the case with all browsers, or do modern browsers clean up properties on elements when they're destoryed? – RightSaidFred Dec 01 '11 at 19:16
  • @RightSaidFred no I'm storing the property in a seperate object and then put the index/key on the dom element – Raynos Dec 01 '11 at 19:20
  • Sorry, my mind was on my comment below Andy E's answer. Didn't make sense up here. I was thinking of data placed directly on the element as opposed to being mapped to a separate object. – RightSaidFred Dec 01 '11 at 19:21

1 Answers1

4

In an article I wrote recently, ES 6 - A quick look at weak maps, I explained how jQuery is able to make data() leak free. It basically generates an expando property name, jQuery.expando. When you attach data to an element, the data is pushed to an internal cache array, and the element is given the expando property with a value of the index of the data in the cache. Something similar to this:

element[jQuery.expando] = elementId;

The way to prevent circular references is to not attach objects directly to elements as expandos. If a reference to the element remains in code, then that element cannot be garbage collected even if it is removed from the DOM. However, preventing circular references doesn't plug the leak entirely - there's still data left in the array if the element is removed from the DOM and garbage collected. So jQuery clears the array on page unload, as well as removing data from the array if elements are removed from the DOM using its own methods like remove(). It keeps the data alive for detach().

The reason jQuery does this, is because there is no weak map equivalent, it's kind of shimmable in ES 5 but not in ES 3. As explained in my article, WeakMap is made for exactly this kind of situation, but the only available implementation is in Firefox 6 and above and, with the spec not being finalized, even that shouldn't be used in production environments.

Another thing to take from my article is that certain elements will not allow you to attach expando properties — <object> and <embed> are the two culprits named and shamed in the jQuery source code. For these element's, you're pretty much screwed and jQuery just will not let you use data on them.


Basic circular reference memory leaks occur in reference counted implementations when two object's properties hold direct references to each other. So DOMObject holds a reference to JSObject and vice versa. Assuming there are no other references to either object, they'd both have a permanent reference count of 1 and the GC would not mark them for collection.

Older browsers (IE6) wouldn't break these circular references, even on page unload, whilst newer browsers are able to break many of these circular references by recognizing the patterns that cause them. jQuery.cache and similar patterns partially void memory leaks because DOMObject never holds a reference to JSObject so, even when JSObject holds a reference to DOMObject, the GC can still mark JSObject for collection when there are no more references to it. Once the GC has collected JSObject, the reference count for DOMObject will be reduced, freeing that up for collection also.

Although IE 8+ and other reference counting browsers may be able to break many circular reference patterns (around 400 were fixed for IE 8), the likelihood of leaks is only reduced. For instance, I've seen a huge leak in one of my own apps in IE 8, when working with script elements and JSONP. The best solution is to plan for the worst and, without WeakMap(), the best you can do is use the jQuery data pattern. Sure, you might be risking having orphaned objects, but this is the lesser of two evils.

Andy E
  • 338,112
  • 86
  • 474
  • 445
  • So why does jQuery bother with the weak map? If they have a global reference to the data anyway via `jQuery.cache`, which means they need to manually clean up, then what's the difference between that and just placing the data directly on the element and cleaning that? At least I would assume that data stored directly on the element will get cleaned up by *some* browsers when that element is destroyed, right? – RightSaidFred Dec 01 '11 at 19:14
  • @RightSaidFred it creates a circular reference that older IE's can't deal with. I think this a balance between leak a lot of memory in old IE and leak some memory in all browsers – Raynos Dec 01 '11 at 19:30
  • @Raynos: But I mean given that jQuery needs to do a cleanup anyway, then it would be cleaning `elem.data` instead of `jQuery.cache[elem.expando]`. Either way it's doing housekeeping to free the memory, so I guess I just don't understand the ultimate benefit of the abstraction. – RightSaidFred Dec 01 '11 at 19:41
  • @Raynos: So as it relates to your question, I'd have to wonder if a WeakMap is really needed. If modern browsers properly clean up elements that have been destroyed, I'd say that the map would be unnecessary. I've just never tested to see if browsers do clean up. – RightSaidFred Dec 01 '11 at 20:14
  • @RightSaidFred: It's a case of implementing the most optimal solution. As @\Raynos said, circular references are a danger when attaching objects to elements. Using the cache avoids those circular references, which can't be cleaned up properly if the element is removed using standard DOM methods - which many devs still use as an optimization over jQuery methods. Worst case scenario without the cache is a memory leak, worst case scenario with the cache is orphaned data which will be cleaned up on unload. – Andy E Dec 01 '11 at 21:23
  • @AndyE: But isn't IE6 the last remaining browser that doesn't clean up memory on unload, or does IE7 suffer as well? I'm pretty sure all other browsers will take care of any leaks on unload. And I have the idea in my head that modern browsers properly clean up elements once they're dereferenced from the DOM (and not referenced in JS), though again I haven't tested this. – RightSaidFred Dec 01 '11 at 21:29
  • @RightSaidFred: all browsers can suffer memory leaks, IE was not the only guilty party. Sure, leaks can be plugged by the vendors but sometimes they can't be detected easily with reference counting. It's also worth noting that clearing up on *unload* isn't going to help for intensive, single page web apps that live for a long time. – Andy E Dec 01 '11 at 22:59
  • @AndyE: That's a good point about long life single page apps, but then you have the same problem for orphaned data. Yes all browsers can suffer leaks, but I thought that only IE6 (and maybe IE7) didn't clean up on unload. So I guess I just don't see how a circular reference made in `jQuery.cache` is better than a circular reference made from a DOM element, especially if it's true that the newer browsers will ensure that destroyed elements don't keep their references. – RightSaidFred Dec 01 '11 at 23:15
  • ...anyway, don't want to be a bother. I should probably do some testing of my own someday. – RightSaidFred Dec 01 '11 at 23:16
  • V8 has it with the --harmony or --harmony_weakmap flag prior to 3.7. I am making full use of Proxy and WeakMap in Node. 3.7-3.8 fixed most/all of the issues also. –  Dec 02 '11 at 09:02
  • @RightSaidFred: it's no bother :-) just an FYI, though, a circular reference can't be made with `jQuery.cache` because the DOM object never directly references the JS object. I updated my answer with a little more detail on circular reference memory leak patterns. – Andy E Dec 02 '11 at 09:26
  • @benvie: that's very cool :-) I suppose with Node, you don't have the danger of potentially changing implementations. If the specification changes, you can update your code and Node at the same time. On the client, however, you have to worry about which browsers have which implementation, which can cause headaches :-) – Andy E Dec 02 '11 at 09:29
  • @AndyE: Oops, yes I didn't mean to say circular, but rather an orphaned reference in `jQuery.cache` vs a circular reference on a DOM element. :) – RightSaidFred Dec 02 '11 at 14:52