Use ES6 Map in ReactComponentTreeHook if available (#7491)

* Use ES6 Map in ReactComponentTreeHook if available

* Make getRootIDs fast again

* Only use native Map
This commit is contained in:
Dan Abramov
2016-08-13 21:43:18 +01:00
committed by GitHub
parent fdc91e016f
commit db452bd20b
3 changed files with 194 additions and 24 deletions
+71 -14
View File
@@ -16,9 +16,46 @@ var ReactCurrentOwner = require('ReactCurrentOwner');
var invariant = require('invariant');
var warning = require('warning');
var itemByKey = {};
var unmountedIDs = {};
var rootIDs = {};
function isNative(fn) {
// Based on isNative() from Lodash
var funcToString = Function.prototype.toString;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var reIsNative = RegExp('^' + funcToString
// Take an example native function source for comparison
.call(hasOwnProperty)
// Strip regex characters so we can use it for regex
.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
// Remove hasOwnProperty from the template to make it generic
.replace(
/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,
'$1.*?'
) + '$'
);
try {
var source = funcToString.call(fn);
return reIsNative.test(source);
} catch (err) {
return false;
}
}
var itemMap;
var itemByKey;
var canUseMap = (
typeof Array.from === 'function' &&
typeof Map === 'function' &&
isNative(Map)
);
if (canUseMap) {
itemMap = new Map();
} else {
itemByKey = {};
}
var unmountedIDs = [];
var rootIDs = [];
// Use non-numeric keys to prevent V8 performance issues:
// https://github.com/facebook/react/pull/7232
@@ -30,18 +67,24 @@ function getIDFromKey(key) {
}
function get(id) {
if (canUseMap) {
return itemMap.get(id);
}
var key = getKeyFromID(id);
return itemByKey[key];
}
function remove(id) {
if (canUseMap) {
itemMap.delete(id);
return;
}
var key = getKeyFromID(id);
delete itemByKey[key];
}
function create(id, element, parentID) {
var key = getKeyFromID(id);
itemByKey[key] = {
var item = {
element,
parentID,
text: null,
@@ -49,6 +92,12 @@ function create(id, element, parentID) {
isMounted: false,
updateCount: 0,
};
if (canUseMap) {
itemMap.set(id, item);
return;
}
var key = getKeyFromID(id);
itemByKey[key] = item;
}
function purgeDeep(id) {
@@ -144,10 +193,6 @@ var ReactComponentTreeHook = {
onBeforeMountComponent(id, element, parentID) {
create(id, element, parentID);
if (parentID === 0) {
rootIDs[id] = true;
}
},
onBeforeUpdateComponent(id, element) {
@@ -163,6 +208,9 @@ var ReactComponentTreeHook = {
onMountComponent(id) {
var item = get(id);
item.isMounted = true;
if (item.parentID === 0) {
rootIDs.push(id);
}
},
onUpdateComponent(id) {
@@ -184,9 +232,14 @@ var ReactComponentTreeHook = {
// got a chance to mount, but it still gets an unmounting event during
// the error boundary cleanup.
item.isMounted = false;
if (item.parentID === 0) {
var indexInRootIDs = rootIDs.indexOf(id);
if (indexInRootIDs !== -1) {
rootIDs.splice(indexInRootIDs, 1);
}
}
}
unmountedIDs[id] = true;
delete rootIDs[id];
unmountedIDs.push(id);
},
purgeUnmountedComponents() {
@@ -195,10 +248,11 @@ var ReactComponentTreeHook = {
return;
}
for (var id in unmountedIDs) {
for (var i = 0; i < unmountedIDs.length; i++) {
var id = unmountedIDs[i];
purgeDeep(id);
}
unmountedIDs = {};
unmountedIDs.length = 0;
},
isMounted(id) {
@@ -292,10 +346,13 @@ var ReactComponentTreeHook = {
},
getRootIDs() {
return Object.keys(rootIDs);
return rootIDs;
},
getRegisteredIDs() {
if (canUseMap) {
return Array.from(itemMap.keys());
}
return Object.keys(itemByKey).map(getIDFromKey);
},
};
@@ -46,14 +46,19 @@ describe('ReactComponentTreeHook', () => {
}
}
function expectWrapperTreeToEqual(expectedTree) {
function expectWrapperTreeToEqual(expectedTree, andStayMounted) {
ReactComponentTreeTestUtils.expectTree(rootInstance._debugID, {
displayName: 'Wrapper',
children: expectedTree ? [expectedTree] : [],
});
var rootDisplayNames = ReactComponentTreeTestUtils.getRootDisplayNames();
var registeredDisplayNames = ReactComponentTreeTestUtils.getRegisteredDisplayNames();
if (!expectedTree) {
expect(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
expect(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
expect(rootDisplayNames).toEqual([]);
expect(registeredDisplayNames).toEqual([]);
} else if (andStayMounted) {
expect(rootDisplayNames).toContain('Wrapper');
expect(registeredDisplayNames).toContain('Wrapper');
}
}
@@ -64,12 +69,12 @@ describe('ReactComponentTreeHook', () => {
// Mount a new tree or update the existing tree.
ReactDOM.render(<Wrapper />, node);
expectWrapperTreeToEqual(expectedTree);
expectWrapperTreeToEqual(expectedTree, true);
// Purging should have no effect
// on the tree we expect to see.
ReactComponentTreeHook.purgeUnmountedComponents();
expectWrapperTreeToEqual(expectedTree);
expectWrapperTreeToEqual(expectedTree, true);
});
// Unmounting the root node should purge
@@ -1864,4 +1869,107 @@ describe('ReactComponentTreeHook', () => {
ReactDOM.render(<Foo />, el);
});
});
describe('in environment without Map and Array.from', () => {
var realMap;
var realArrayFrom;
beforeEach(() => {
realMap = global.Map;
realArrayFrom = Array.from;
global.Map = undefined;
Array.from = undefined;
jest.resetModuleRegistry();
React = require('React');
ReactDOM = require('ReactDOM');
ReactDOMServer = require('ReactDOMServer');
ReactInstanceMap = require('ReactInstanceMap');
ReactComponentTreeHook = require('ReactComponentTreeHook');
ReactComponentTreeTestUtils = require('ReactComponentTreeTestUtils');
});
afterEach(() => {
global.Map = realMap;
Array.from = realArrayFrom;
});
it('works', () => {
class Qux extends React.Component {
render() {
return null;
}
}
function Foo() {
return {
render() {
return <Qux />;
},
};
}
function Bar({children}) {
return <h1>{children}</h1>;
}
class Baz extends React.Component {
render() {
return (
<div>
<Foo />
<Bar>
<span>Hi,</span>
Mom
</Bar>
<a href="#">Click me.</a>
</div>
);
}
}
var element = <Baz />;
var tree = {
displayName: 'Baz',
element,
children: [{
displayName: 'div',
children: [{
displayName: 'Foo',
element: <Foo />,
children: [{
displayName: 'Qux',
element: <Qux />,
children: [],
}],
}, {
displayName: 'Bar',
children: [{
displayName: 'h1',
children: [{
displayName: 'span',
children: [{
displayName: '#text',
element: 'Hi,',
text: 'Hi,',
}],
}, {
displayName: '#text',
text: 'Mom',
element: 'Mom',
}],
}],
}, {
displayName: 'a',
children: [{
displayName: '#text',
text: 'Click me.',
element: 'Click me.',
}],
}],
}],
};
assertTreeMatches([element, tree]);
});
});
});
@@ -70,14 +70,19 @@ describe('ReactComponentTreeHook', () => {
}
}
function expectWrapperTreeToEqual(expectedTree) {
function expectWrapperTreeToEqual(expectedTree, andStayMounted) {
ReactComponentTreeTestUtils.expectTree(rootInstance._debugID, {
displayName: 'Wrapper',
children: expectedTree ? [expectedTree] : [],
});
var rootDisplayNames = ReactComponentTreeTestUtils.getRootDisplayNames();
var registeredDisplayNames = ReactComponentTreeTestUtils.getRegisteredDisplayNames();
if (!expectedTree) {
expect(ReactComponentTreeTestUtils.getRootDisplayNames()).toEqual([]);
expect(ReactComponentTreeTestUtils.getRegisteredDisplayNames()).toEqual([]);
expect(rootDisplayNames).toEqual([]);
expect(registeredDisplayNames).toEqual([]);
} else if (andStayMounted) {
expect(rootDisplayNames).toContain('Wrapper');
expect(registeredDisplayNames).toContain('Wrapper');
}
}
@@ -88,12 +93,12 @@ describe('ReactComponentTreeHook', () => {
// Mount a new tree or update the existing tree.
ReactNative.render(<Wrapper />, 1);
expectWrapperTreeToEqual(expectedTree);
expectWrapperTreeToEqual(expectedTree, true);
// Purging should have no effect
// on the tree we expect to see.
ReactComponentTreeHook.purgeUnmountedComponents();
expectWrapperTreeToEqual(expectedTree);
expectWrapperTreeToEqual(expectedTree, true);
});
// Unmounting the root node should purge