-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ko.utils.domData storage scheme requires explicit cleanup #2141
Conversation
The fix could require an explicit opt-in (the old behavior would still be the default): @@ -685,17 +685,30 @@
var uniqueId = 0;
var dataStoreKeyExpandoPropertyName = "__ko__" + (new Date).getTime();
var dataStore = {};
+ var dataStoreInUse = false;
function getAll(node, createIfNotFound) {
var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
- var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
- if (!hasExistingDataStore) {
- if (!createIfNotFound)
- return undefined;
- dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
- dataStore[dataStoreKey] = {};
+
+ if (dataStore) {
+ dataStoreInUse = true;
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
+ if (!hasExistingDataStore) {
+ if (!createIfNotFound)
+ return undefined;
+ dataStoreKey = node[dataStoreKeyExpandoPropertyName] = "ko" + uniqueId++;
+ dataStore[dataStoreKey] = {};
+ }
+ return dataStore[dataStoreKey];
}
- return dataStore[dataStoreKey];
+ else
+ {
+ var data = dataStoreKey;
+ if (!data && createIfNotFound) {
+ data = node[dataStoreKeyExpandoPropertyName] = {};
+ }
+ return data;
+ }
}
return {
@@ -715,7 +728,8 @@
clear: function (node) {
var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
if (dataStoreKey) {
- delete dataStore[dataStoreKey];
+ if (dataStore)
+ delete dataStore[dataStoreKey];
node[dataStoreKeyExpandoPropertyName] = null;
return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
}
@@ -724,12 +738,18 @@
nextKey: function () {
return (uniqueId++) + dataStoreKeyExpandoPropertyName;
+ },
+ storeOnNode: function() {
+ if (dataStoreInUse)
+ throw "Cannot change data storage scheme after it has been initialized";
+ dataStore = null;
}
};
})();
ko.exportSymbol('utils.domData', ko.utils.domData);
ko.exportSymbol('utils.domData.clear', ko.utils.domData.clear); // Exporting only so specs can clear up after themselves fully
+ko.exportSymbol('utils.domData.storeOnNode', ko.utils.domData.storeOnNode);
ko.utils.domNodeDisposal = new (function () {
var domDataKey = ko.utils.domData.nextKey() |
Also, would you guys recommend entering this problem as a tko issue (the same code exists there)? |
I'd certainly like this to not be an issue in tko 😀 ... but I still don't quite understand the nature of the issue -- will need to stare at it a bit before I can make a meaningful comment. |
... why not use |
Hey Brian, ko.removeNode() would certainly clean up DOM data, but it won't solve the issue for us. We are using Knockout as a template engine for the internal template within a custom element/web component. The page author using the component is not supposed to know or care that KO is managing the template (that decision was made by the web component author). They are just seeing a custom element in the DOM (for example, . Web component spec does not call for any explicit cleanup, and the assumption is that the garbage collection will just work after the custom element node is removed from the document. I agree that current Knockout users are aware that they need to clean up Knockout explicitly. However, I also think that that it would be great if KO supported being used as a template engine within web components, and for that use case supporting cleanup by garbage collection is a must have. Thanks, |
Got it. Is using MutationObservers to see when elements are removed an option? |
Thanks, Brian, Custom elements are already being notified when they are being attached or detached (the custom component polyfill uses mutation observers to implement that). The problem is that detach is not equivalent to destroy - it could easily be a temporary detach or a move. Allowing garbage collection to take care of things is the best solution, as nobody will be holding references to the custom element if they are not planning to re-insert it into the document. Max |
The current |
Maybe using WeakMap @mbest? |
+1 to adding an option to switch domData implementation for those who do not care about supporting IE 6 and 7. Regarding the WeakMap: I am not sure it provides much advantage over storing data in the node's expando. The keys in WeakMap have to be objects, so you would be forced to store an object on the DOM node in any case. |
If using a WeakMap, the DOM node would be the key. The problem with WeakMap is browser support ... IE < 11. |
... another option I toyed with is to use a Symbol property on the node. |
So TKO prefers WeakSet if supported, hopefully resolving this issue. |
Thanks, Brian. The tko change looks good. |
Hey Brian, Any chance of backporting the tko fix to the classic Knockout? We have a project that depends on using KO as a template manager with Web Components/Custom Elements, and the only other avenue would be forking. |
It's a simple patch and I've no issue with it being in 3.5 -- @mbest ? |
I'll see what it would involve. |
@mbest This is the patch for tko: The API stays the same. |
We would settle for an opt-in API for this change too if needed. Thanks, |
I'm trying to use your code in Knockout and ran into this issue in IE 11:
Any ideas? |
It looks like IE and Edge don't support using a node from another window in a WeakMap. Here is a workaround (needed on every access):
|
Thanks @mbest — I've opened an issue in TKO and will mull & merge a PR. |
src/utils.domData.js
Outdated
if (window['WeakMap']) { | ||
getDataForNode = function (node, createIfNotFound) { | ||
var ownerDoc = node.ownerDocument, | ||
dataStore = ownerDoc[dataStoreKeyExpandoPropertyName] || (ownerDoc[dataStoreKeyExpandoPropertyName] = new ownerDoc.defaultView['WeakMap']()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good job finding this edge case (pardon the pun)
Maybe it's just simper to use @mstarets's technique of storing the data on the node itself. The old method would only need to be used in old-IE. |
Yeah an IE test is simpler than this ownerdox logic. Does latest Edge fail too? Is it a known issue? |
There doesn't seem to be a report of it. https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/?page=1&q=weakmap |
This version doesn't use |
I think this is the most practical approach. I think we should be explicit about what happens with Edge. I wonder if there's a bug report / reporting facility / this would be considered a bug. |
if (getAll(node, false) === undefined) | ||
return; | ||
var getDataForNode, clear; | ||
if (!ko.utils.ieVersion) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this should be
if (!ko.utils.ieVersion && 'WeakMap' in window) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also probably note the bug report here or explain why IE's WeakMap
is broken.
I've reported the |
Noting chakra-core/ChakraCore#2983 |
This problem has been previously brought up in the rejected Issue #1695. Our case idifferent, however: we have no way of ensuring that the explicit ko.cleanNode() is always invoked for the removed element.
Also note that jQuery has switched to storing data on the expando - jquery/jquery#1734. And they had long been advocating for the same data storage solution as Knockout's...
Or use case is as follows:
Unfortunately, Knockout's domData maintains references to to all DOM nodes where bindings were applied, so all DOM nodes within the Knockout template remains in memory heap.
Here is a jsFiddle that demonstrates the issue: https://jsfiddle.net/mstarets/4nfsebec/5/. It needs to be run in Chrome (otherwise you will need to include the Custom Element polyfill). After you run the page, click on 'Create Custom Element button, then on 'Remove Custom Element' button. Repeat this sequence 2-3 times. Open Developer Tools, go to the 'Profiles' tab and take a heap snapshot. Note that there are several hundred of HTMLButtonElements that have been leaked. Now uncomment the line calling clearDataForButtons() and repeat the same test. Note that the button elements are no longer leaked.