@samitouri / QOS-React / commits / db452bd20b

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