@samitouri / QOS-React-2 / commits / d93761af62
Fork 0

The library for web and native user interfaces.

Enforce nodeCache validity

A node is considered valid if it 1. has the expected ID (more of a sanity check than something that is ever likely to go wrong), and 2. is contained by a currently mounted container. When these requirements are met, we can be confident that `ReactMount.findReactRenderedDOMNodeSlow(node.id) === node`, which is important for cache consistency because `findReactRenderedDOMNodeSlow` is what we fall back to when we don't find a node in the cache. Point 2 is a subtle requirement, because it allows nodes to be valid even if they are not currently contained by a document. Rendering into a detached node is okay, in other words (which is something that `document.getElementById` never properly accounted for). Containment testing takes linear time in the depth of the DOM, which sounds unfortunate until you realize that virtually all browsers support a native `ancestor.contains(descendant)` method, and in practice the vast majority of nodes are either orphaned with `.parentNode === null` or not very deep relative to their container.

CommitSyncScript committed Jun 25, 2013 at 13:56 UTC d93761af6253aa994660eb04be9313cddc920844
1 file changed +85 -18
src/core/ReactID.js
+85 -18
@@ -19,6 +19,7 @@
19
20 "use strict";
21
22 +var invariant = require('invariant');
23 var ReactMount = require('ReactMount');
24 var ATTR_NAME = 'id';
25 var nodeCache = {};
@@ -34,20 +35,34 @@ var nodeCache = {};
35 * @return {string} ID of the supplied `domNode`.
36 */
37 function getID(node) {
37 - if (node && node.getAttributeNode) {
38 - var attributeNode = node.getAttributeNode(ATTR_NAME);
39 - if (attributeNode) {
40 - var id = attributeNode.value;
41 - if (id) {
42 - // Assume that any node previously cached with this ID has since
43 - // become invalid and should be replaced. TODO Enforce this.
44 - nodeCache[id] = node;
38 + var id = internalGetID(node);
39 + if (id) {
40 + if (nodeCache.hasOwnProperty(id)) {
41 + var cached = nodeCache[id];
42 + if (cached !== node) {
43 + invariant(
44 + !isValid(cached, id),
45 + 'ReactID: Two valid but unequal nodes with the same `%s`: %s',
46 + ATTR_NAME, id
47 + );
48
46 - return id;
49 + nodeCache[id] = node;
50 }
51 + } else {
52 + nodeCache[id] = node;
53 }
54 }
55
56 + return id;
57 +}
58 +
59 +function internalGetID(node) {
60 + if (node && node.getAttributeNode) {
61 + var attributeNode = node.getAttributeNode(ATTR_NAME);
62 + if (attributeNode) {
63 + return attributeNode.value || '';
64 + }
65 + }
66 return '';
67 }
68
@@ -58,7 +73,7 @@ function getID(node) {
73 * @param {string} id The value of the ID attribute.
74 */
75 function setID(node, id) {
61 - var oldID = getID(node);
76 + var oldID = internalGetID(node);
77 if (oldID !== id) {
78 delete nodeCache[oldID];
79 }
@@ -74,18 +89,70 @@ function setID(node, id) {
89 * @internal
90 */
91 function getNode(id) {
77 - if (!nodeCache[id]) {
78 - nodeCache[id] =
79 - document.getElementById(id) || // TODO Quit using getElementById.
80 - ReactMount.findReactRenderedDOMNodeSlow(id);
92 + if (nodeCache.hasOwnProperty(id)) {
93 + var node = nodeCache[id];
94 + if (isValid(node, id)) {
95 + return node;
96 + }
97 + }
98 +
99 + return nodeCache[id] =
100 + document.getElementById(id) || // TODO Quit using getElementById.
101 + ReactMount.findReactRenderedDOMNodeSlow(id);
102 +}
103 +
104 +/**
105 + * A node is "valid" if it is contained by a currently mounted container.
106 + *
107 + * This means that the node does not have to be contained by a document in
108 + * order to be considered valid.
109 + *
110 + * @param {?DOMElement} node The candidate DOM node.
111 + * @param {string} id The expected ID of the node.
112 + * @return {boolean} Whether the node is contained by a mounted container.
113 + */
114 +function isValid(node, id) {
115 + if (node) {
116 + invariant(
117 + internalGetID(node) === id,
118 + 'ReactID: Unexpected modification of `%s`',
119 + ATTR_NAME
120 + );
121 +
122 + var container = ReactMount.findReactContainerForID(id);
123 + if (container && contains(container, node)) {
124 + return true;
125 + }
126 + }
127 +
128 + return false;
129 +}
130 +
131 +function contains(ancestor, descendant) {
132 + if (ancestor.contains) {
133 + // Supported natively in virtually all browsers, but not in jsdom.
134 + return ancestor.contains(descendant);
135 + }
136 +
137 + if (descendant === ancestor) {
138 + return true;
139 + }
140 +
141 + if (descendant.nodeType === 3) {
142 + // If descendant is a text node, start from descendant.parentNode
143 + // instead, so that we can assume all ancestors worth considering are
144 + // element nodes with nodeType === 1.
145 + descendant = descendant.parentNode;
146 }
147
83 - var node = nodeCache[id];
84 - if (getID(node) === id) {
85 - return node;
148 + while (descendant && descendant.nodeType === 1) {
149 + if (descendant === ancestor) {
150 + return true;
151 + }
152 + descendant = descendant.parentNode;
153 }
154
88 - return null;
155 + return false;
156 }
157
158 /**