Use ES6 Map in ReactComponentTreeHook if available (#7491)
* Use ES6 Map in ReactComponentTreeHook if available * Make getRootIDs fast again * Only use native Map
Dan Abramov committed
Aug 13, 2016 at 21:43 UTC
db452bd20bb2f4f7f0a5621c07c6eec6dc618179
3 files changed
+194
-24
src/isomorphic/hooks/ReactComponentTreeHook.js
+71
-14
@@ -16,9 +16,46 @@ var ReactCurrentOwner = require('ReactCurrentOwner');
16
var invariant = require('invariant');
17
var warning = require('warning');
18
19
-var itemByKey = {};
20
-var unmountedIDs = {};
21
-var rootIDs = {};
19
+function isNative(fn) {
20
+ // Based on isNative() from Lodash
21
+ var funcToString = Function.prototype.toString;
22
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
23
+ var reIsNative = RegExp('^' + funcToString
24
+ // Take an example native function source for comparison
25
+ .call(hasOwnProperty)
26
+ // Strip regex characters so we can use it for regex
27
+ .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
28
+ // Remove hasOwnProperty from the template to make it generic
29
+ .replace(
30
+ /hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,
31
+ '$1.*?'
32
+ ) + '$'
33
+ );
34
+ try {
35
+ var source = funcToString.call(fn);
36
+ return reIsNative.test(source);
37
+ } catch (err) {
38
+ return false;
39
+ }
40
+}
41
+
42
+var itemMap;
43
+var itemByKey;
44
+
45
+var canUseMap = (
46
+ typeof Array.from === 'function' &&
47
+ typeof Map === 'function' &&
48
+ isNative(Map)
49
+);
50
+
51
+if (canUseMap) {
52
+ itemMap = new Map();
53
+} else {
54
+ itemByKey = {};
55
+}
56
+
57
+var unmountedIDs = [];
58
+var rootIDs = [];
59
60
// Use non-numeric keys to prevent V8 performance issues:
61
// https://github.com/facebook/react/pull/7232
@@ -30,18 +67,24 @@ function getIDFromKey(key) {
67
}
68
69
function get(id) {
70
+ if (canUseMap) {
71
+ return itemMap.get(id);
72
+ }
73
var key = getKeyFromID(id);
74
return itemByKey[key];
75
}
76
77
function remove(id) {
78
+ if (canUseMap) {
79
+ itemMap.delete(id);
80
+ return;
81
+ }
82
var key = getKeyFromID(id);
83
delete itemByKey[key];
84
}
85
86
function create(id, element, parentID) {
43
- var key = getKeyFromID(id);
44
- itemByKey[key] = {
87
+ var item = {
88
element,
89
parentID,
90
text: null,
@@ -49,6 +92,12 @@ function create(id, element, parentID) {
92
isMounted: false,
93
updateCount: 0,
94
};
95
+ if (canUseMap) {
96
+ itemMap.set(id, item);
97
+ return;
98
+ }
99
+ var key = getKeyFromID(id);
100
+ itemByKey[key] = item;
101
}
102
103
function purgeDeep(id) {
@@ -144,10 +193,6 @@ var ReactComponentTreeHook = {
193
194
onBeforeMountComponent(id, element, parentID) {
195
create(id, element, parentID);
147
-
148
- if (parentID === 0) {
149
- rootIDs[id] = true;
150
- }
196
},
197
198
onBeforeUpdateComponent(id, element) {
@@ -163,6 +208,9 @@ var ReactComponentTreeHook = {
208
onMountComponent(id) {
209
var item = get(id);
210
item.isMounted = true;
211
+ if (item.parentID === 0) {
212
+ rootIDs.push(id);
213
+ }
214
},
215
216
onUpdateComponent(id) {
@@ -184,9 +232,14 @@ var ReactComponentTreeHook = {
232
// got a chance to mount, but it still gets an unmounting event during
233
// the error boundary cleanup.
234
item.isMounted = false;
235
+ if (item.parentID === 0) {
236
+ var indexInRootIDs = rootIDs.indexOf(id);
237
+ if (indexInRootIDs !== -1) {
238
+ rootIDs.splice(indexInRootIDs, 1);
239
+ }
240
+ }
241
}
188
- unmountedIDs[id] = true;
189
- delete rootIDs[id];
242
+ unmountedIDs.push(id);
243
},
244
245
purgeUnmountedComponents() {
@@ -195,10 +248,11 @@ var ReactComponentTreeHook = {
248
return;
249
}
250
198
- for (var id in unmountedIDs) {
251
+ for (var i = 0; i < unmountedIDs.length; i++) {
252
+ var id = unmountedIDs[i];
253
purgeDeep(id);
254
}
201
- unmountedIDs = {};
255
+ unmountedIDs.length = 0;
256
},
257
258
isMounted(id) {
@@ -292,10 +346,13 @@ var ReactComponentTreeHook = {
346
},
347
348
getRootIDs() {
295
- return Object.keys(rootIDs);
349
+ return rootIDs;
350
},
351
352
getRegisteredIDs() {
353
+ if (canUseMap) {
354
+ return Array.from(itemMap.keys());
355
+ }
356
return Object.keys(itemByKey).map(getIDFromKey);
357
},
358
};
src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js
+113
-5
@@ -46,14 +46,19 @@ describe('ReactComponentTreeHook', () => {
46
}
47
}
48
49
- function expectWrapperTreeToEqual(expectedTree) {
49
+ function expectWrapperTreeToEqual(expectedTree, andStayMounted) {
50
ReactComponentTreeTestUtils.expectTree(rootInstance._debugID, {
51
displayName: 'Wrapper',
52
children: expectedTree ? [expectedTree] : [],
53
});
54
+ var rootDisplayNames = ReactComponentTreeTestUtils.getRootDisplayNames();
55
+ var registeredDisplayNames = ReactComponentTreeTestUtils.getRegisteredDisplayNames();
56
if (!expectedTree) {
55
- expect(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
56
- expect(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
57
+ expect(rootDisplayNames).toEqual([]);
58
+ expect(registeredDisplayNames).toEqual([]);
59
+ } else if (andStayMounted) {
60
+ expect(rootDisplayNames).toContain('Wrapper');
61
+ expect(registeredDisplayNames).toContain('Wrapper');
62
}
63
}
64
@@ -64,12 +69,12 @@ describe('ReactComponentTreeHook', () => {
69
70
// Mount a new tree or update the existing tree.
71
ReactDOM.render(<Wrapper />, node);
67
- expectWrapperTreeToEqual(expectedTree);
72
+ expectWrapperTreeToEqual(expectedTree, true);
73
74
// Purging should have no effect
75
// on the tree we expect to see.
76
ReactComponentTreeHook.purgeUnmountedComponents();
72
- expectWrapperTreeToEqual(expectedTree);
77
+ expectWrapperTreeToEqual(expectedTree, true);
78
});
79
80
// Unmounting the root node should purge
@@ -1864,4 +1869,107 @@ describe('ReactComponentTreeHook', () => {
1869
ReactDOM.render(<Foo />, el);
1870
});
1871
});
1872
+
1873
+ describe('in environment without Map and Array.from', () => {
1874
+ var realMap;
1875
+ var realArrayFrom;
1876
+
1877
+ beforeEach(() => {
1878
+ realMap = global.Map;
1879
+ realArrayFrom = Array.from;
1880
+
1881
+ global.Map = undefined;
1882
+ Array.from = undefined;
1883
+
1884
+ jest.resetModuleRegistry();
1885
+
1886
+ React = require('React');
1887
+ ReactDOM = require('ReactDOM');
1888
+ ReactDOMServer = require('ReactDOMServer');
1889
+ ReactInstanceMap = require('ReactInstanceMap');
1890
+ ReactComponentTreeHook = require('ReactComponentTreeHook');
1891
+ ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils');
1892
+ });
1893
+
1894
+ afterEach(() => {
1895
+ global.Map = realMap;
1896
+ Array.from = realArrayFrom;
1897
+ });
1898
+
1899
+ it('works', () => {
1900
+ class Qux extends React.Component {
1901
+ render() {
1902
+ return null;
1903
+ }
1904
+ }
1905
+
1906
+ function Foo() {
1907
+ return {
1908
+ render() {
1909
+ return <Qux />;
1910
+ },
1911
+ };
1912
+ }
1913
+ function Bar({children}) {
1914
+ return <h1>{children}</h1>;
1915
+ }
1916
+ class Baz extends React.Component {
1917
+ render() {
1918
+ return (
1919
+ <div>
1920
+ <Foo />
1921
+ <Bar>
1922
+ <span>Hi,</span>
1923
+ Mom
1924
+ </Bar>
1925
+ <a href="#">Click me.</a>
1926
+ </div>
1927
+ );
1928
+ }
1929
+ }
1930
+
1931
+ var element = <Baz />;
1932
+ var tree = {
1933
+ displayName: 'Baz',
1934
+ element,
1935
+ children: [{
1936
+ displayName: 'div',
1937
+ children: [{
1938
+ displayName: 'Foo',
1939
+ element: <Foo />,
1940
+ children: [{
1941
+ displayName: 'Qux',
1942
+ element: <Qux />,
1943
+ children: [],
1944
+ }],
1945
+ }, {
1946
+ displayName: 'Bar',
1947
+ children: [{
1948
+ displayName: 'h1',
1949
+ children: [{
1950
+ displayName: 'span',
1951
+ children: [{
1952
+ displayName: '#text',
1953
+ element: 'Hi,',
1954
+ text: 'Hi,',
1955
+ }],
1956
+ }, {
1957
+ displayName: '#text',
1958
+ text: 'Mom',
1959
+ element: 'Mom',
1960
+ }],
1961
+ }],
1962
+ }, {
1963
+ displayName: 'a',
1964
+ children: [{
1965
+ displayName: '#text',
1966
+ text: 'Click me.',
1967
+ element: 'Click me.',
1968
+ }],
1969
+ }],
1970
+ }],
1971
+ };
1972
+ assertTreeMatches([element, tree]);
1973
+ });
1974
+ });
1975
});
src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.native.js
+10
-5
@@ -70,14 +70,19 @@ describe('ReactComponentTreeHook', () => {
70
}
71
}
72
73
- function expectWrapperTreeToEqual(expectedTree) {
73
+ function expectWrapperTreeToEqual(expectedTree, andStayMounted) {
74
ReactComponentTreeTestUtils.expectTree(rootInstance._debugID, {
75
displayName: 'Wrapper',
76
children: expectedTree ? [expectedTree] : [],
77
});
78
+ var rootDisplayNames = ReactComponentTreeTestUtils.getRootDisplayNames();
79
+ var registeredDisplayNames = ReactComponentTreeTestUtils.getRegisteredDisplayNames();
80
if (!expectedTree) {
79
- expect(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
80
- expect(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
81
+ expect(rootDisplayNames).toEqual([]);
82
+ expect(registeredDisplayNames).toEqual([]);
83
+ } else if (andStayMounted) {
84
+ expect(rootDisplayNames).toContain('Wrapper');
85
+ expect(registeredDisplayNames).toContain('Wrapper');
86
}
87
}
88
@@ -88,12 +93,12 @@ describe('ReactComponentTreeHook', () => {
93
94
// Mount a new tree or update the existing tree.
95
ReactNative.render(<Wrapper />, 1);
91
- expectWrapperTreeToEqual(expectedTree);
96
+ expectWrapperTreeToEqual(expectedTree, true);
97
98
// Purging should have no effect
99
// on the tree we expect to see.
100
ReactComponentTreeHook.purgeUnmountedComponents();
96
- expectWrapperTreeToEqual(expectedTree);
101
+ expectWrapperTreeToEqual(expectedTree, true);
102
});
103
104
// Unmounting the root node should purge