Skip to content

Commit db452bd

Browse files
authored
Use ES6 Map in ReactComponentTreeHook if available (#7491)
* Use ES6 Map in ReactComponentTreeHook if available * Make getRootIDs fast again * Only use native Map
1 parent fdc91e0 commit db452bd

File tree

3 files changed

+194
-24
lines changed

3 files changed

+194
-24
lines changed

src/isomorphic/hooks/ReactComponentTreeHook.js

+71-14
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,46 @@ var ReactCurrentOwner = require('ReactCurrentOwner');
1616
var invariant = require('invariant');
1717
var warning = require('warning');
1818

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 = [];
2259

2360
// Use non-numeric keys to prevent V8 performance issues:
2461
// https://github.com/facebook/react/pull/7232
@@ -30,25 +67,37 @@ function getIDFromKey(key) {
3067
}
3168

3269
function get(id) {
70+
if (canUseMap) {
71+
return itemMap.get(id);
72+
}
3373
var key = getKeyFromID(id);
3474
return itemByKey[key];
3575
}
3676

3777
function remove(id) {
78+
if (canUseMap) {
79+
itemMap.delete(id);
80+
return;
81+
}
3882
var key = getKeyFromID(id);
3983
delete itemByKey[key];
4084
}
4185

4286
function create(id, element, parentID) {
43-
var key = getKeyFromID(id);
44-
itemByKey[key] = {
87+
var item = {
4588
element,
4689
parentID,
4790
text: null,
4891
childIDs: [],
4992
isMounted: false,
5093
updateCount: 0,
5194
};
95+
if (canUseMap) {
96+
itemMap.set(id, item);
97+
return;
98+
}
99+
var key = getKeyFromID(id);
100+
itemByKey[key] = item;
52101
}
53102

54103
function purgeDeep(id) {
@@ -144,10 +193,6 @@ var ReactComponentTreeHook = {
144193

145194
onBeforeMountComponent(id, element, parentID) {
146195
create(id, element, parentID);
147-
148-
if (parentID === 0) {
149-
rootIDs[id] = true;
150-
}
151196
},
152197

153198
onBeforeUpdateComponent(id, element) {
@@ -163,6 +208,9 @@ var ReactComponentTreeHook = {
163208
onMountComponent(id) {
164209
var item = get(id);
165210
item.isMounted = true;
211+
if (item.parentID === 0) {
212+
rootIDs.push(id);
213+
}
166214
},
167215

168216
onUpdateComponent(id) {
@@ -184,9 +232,14 @@ var ReactComponentTreeHook = {
184232
// got a chance to mount, but it still gets an unmounting event during
185233
// the error boundary cleanup.
186234
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+
}
187241
}
188-
unmountedIDs[id] = true;
189-
delete rootIDs[id];
242+
unmountedIDs.push(id);
190243
},
191244

192245
purgeUnmountedComponents() {
@@ -195,10 +248,11 @@ var ReactComponentTreeHook = {
195248
return;
196249
}
197250

198-
for (var id in unmountedIDs) {
251+
for (var i = 0; i < unmountedIDs.length; i++) {
252+
var id = unmountedIDs[i];
199253
purgeDeep(id);
200254
}
201-
unmountedIDs = {};
255+
unmountedIDs.length = 0;
202256
},
203257

204258
isMounted(id) {
@@ -292,10 +346,13 @@ var ReactComponentTreeHook = {
292346
},
293347

294348
getRootIDs() {
295-
return Object.keys(rootIDs);
349+
return rootIDs;
296350
},
297351

298352
getRegisteredIDs() {
353+
if (canUseMap) {
354+
return Array.from(itemMap.keys());
355+
}
299356
return Object.keys(itemByKey).map(getIDFromKey);
300357
},
301358
};

src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js

+113-5
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,19 @@ describe('ReactComponentTreeHook', () => {
4646
}
4747
}
4848

49-
function expectWrapperTreeToEqual(expectedTree) {
49+
function expectWrapperTreeToEqual(expectedTree, andStayMounted) {
5050
ReactComponentTreeTestUtils.expectTree(rootInstance._debugID, {
5151
displayName: 'Wrapper',
5252
children: expectedTree ? [expectedTree] : [],
5353
});
54+
var rootDisplayNames = ReactComponentTreeTestUtils.getRootDisplayNames();
55+
var registeredDisplayNames = ReactComponentTreeTestUtils.getRegisteredDisplayNames();
5456
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');
5762
}
5863
}
5964

@@ -64,12 +69,12 @@ describe('ReactComponentTreeHook', () => {
6469

6570
// Mount a new tree or update the existing tree.
6671
ReactDOM.render(<Wrapper />, node);
67-
expectWrapperTreeToEqual(expectedTree);
72+
expectWrapperTreeToEqual(expectedTree, true);
6873

6974
// Purging should have no effect
7075
// on the tree we expect to see.
7176
ReactComponentTreeHook.purgeUnmountedComponents();
72-
expectWrapperTreeToEqual(expectedTree);
77+
expectWrapperTreeToEqual(expectedTree, true);
7378
});
7479

7580
// Unmounting the root node should purge
@@ -1864,4 +1869,107 @@ describe('ReactComponentTreeHook', () => {
18641869
ReactDOM.render(<Foo />, el);
18651870
});
18661871
});
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+
});
18671975
});

src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.native.js

+10-5
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,19 @@ describe('ReactComponentTreeHook', () => {
7070
}
7171
}
7272

73-
function expectWrapperTreeToEqual(expectedTree) {
73+
function expectWrapperTreeToEqual(expectedTree, andStayMounted) {
7474
ReactComponentTreeTestUtils.expectTree(rootInstance._debugID, {
7575
displayName: 'Wrapper',
7676
children: expectedTree ? [expectedTree] : [],
7777
});
78+
var rootDisplayNames = ReactComponentTreeTestUtils.getRootDisplayNames();
79+
var registeredDisplayNames = ReactComponentTreeTestUtils.getRegisteredDisplayNames();
7880
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');
8186
}
8287
}
8388

@@ -88,12 +93,12 @@ describe('ReactComponentTreeHook', () => {
8893

8994
// Mount a new tree or update the existing tree.
9095
ReactNative.render(<Wrapper />, 1);
91-
expectWrapperTreeToEqual(expectedTree);
96+
expectWrapperTreeToEqual(expectedTree, true);
9297

9398
// Purging should have no effect
9499
// on the tree we expect to see.
95100
ReactComponentTreeHook.purgeUnmountedComponents();
96-
expectWrapperTreeToEqual(expectedTree);
101+
expectWrapperTreeToEqual(expectedTree, true);
97102
});
98103

99104
// Unmounting the root node should purge

0 commit comments

Comments
 (0)