refactor of ReactComponentTreeHook to isolate dev methods

This commit is contained in:
Dominic Gannaway
2017-03-02 16:15:37 +00:00
parent a190cfce29
commit 0e23042e4b
5 changed files with 393 additions and 386 deletions
@@ -18,12 +18,7 @@ import type { DebugID } from 'ReactInstanceType';
const ReactDebugCurrentFrame = {};
if (__DEV__) {
var {
getStackAddendumByID,
getStackAddendumByWorkInProgressFiber,
getCurrentStackAddendum,
} = require('ReactComponentTreeHook');
var ReactComponentTreeHook = require('ReactComponentTreeHook');
// Component that is being worked on
ReactDebugCurrentFrame.current = (null : Fiber | DebugID | null);
@@ -38,16 +33,16 @@ if (__DEV__) {
if (typeof current === 'number') {
// DebugID from Stack.
const debugID = current;
stack = getStackAddendumByID(debugID);
stack = (ReactComponentTreeHook: any).getStackAddendumByID(debugID);
} else if (typeof current.tag === 'number') {
// This is a Fiber.
// The stack will only be correct if this is a work in progress
// version and we're calling it during reconciliation.
const workInProgress = current;
stack = getStackAddendumByWorkInProgressFiber(workInProgress);
stack = ReactComponentTreeHook.getStackAddendumByWorkInProgressFiber(workInProgress);
}
} else if (element !== null) {
stack = getCurrentStackAddendum(element);
stack = (ReactComponentTreeHook: any).getCurrentStackAddendum(element);
}
return stack;
};
+376 -364
View File
@@ -12,7 +12,11 @@
'use strict';
var ReactCurrentOwner = require('ReactCurrentOwner');
import type { ReactElement, Source } from 'ReactElementType';
import type { DebugID } from 'ReactInstanceType';
import type { Fiber } from 'ReactFiber';
var getComponentName = require('getComponentName');
var ReactTypeOfWork = require('ReactTypeOfWork');
var {
IndeterminateComponent,
@@ -21,144 +25,6 @@ var {
HostComponent,
} = ReactTypeOfWork;
var getComponentName = require('getComponentName');
var invariant = require('fbjs/lib/invariant');
var warning = require('fbjs/lib/warning');
import type { ReactElement, Source } from 'ReactElementType';
import type { DebugID } from 'ReactInstanceType';
import type { Fiber } from 'ReactFiber';
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 canUseCollections = (
// Array.from
typeof Array.from === 'function' &&
// Map
typeof Map === 'function' &&
isNative(Map) &&
// Map.prototype.keys
Map.prototype != null &&
typeof Map.prototype.keys === 'function' &&
isNative(Map.prototype.keys) &&
// Set
typeof Set === 'function' &&
isNative(Set) &&
// Set.prototype.keys
Set.prototype != null &&
typeof Set.prototype.keys === 'function' &&
isNative(Set.prototype.keys)
);
var setItem;
var getItem;
var removeItem;
var getItemIDs;
var addRoot;
var removeRoot;
var getRootIDs;
if (canUseCollections) {
var itemMap = new Map();
var rootIDSet = new Set();
setItem = function(id, item) {
itemMap.set(id, item);
};
getItem = function(id) {
return itemMap.get(id);
};
removeItem = function(id) {
itemMap.delete(id);
};
getItemIDs = function() {
return Array.from(itemMap.keys());
};
addRoot = function(id) {
rootIDSet.add(id);
};
removeRoot = function(id) {
rootIDSet.delete(id);
};
getRootIDs = function() {
return Array.from(rootIDSet.keys());
};
} else {
var itemByKey = {};
var rootByKey = {};
// Use non-numeric keys to prevent V8 performance issues:
// https://github.com/facebook/react/pull/7232
var getKeyFromID = function(id: DebugID): string {
return '.' + id;
};
var getIDFromKey = function(key: string): DebugID {
return parseInt(key.substr(1), 10);
};
setItem = function(id, item) {
var key = getKeyFromID(id);
itemByKey[key] = item;
};
getItem = function(id) {
var key = getKeyFromID(id);
return itemByKey[key];
};
removeItem = function(id) {
var key = getKeyFromID(id);
delete itemByKey[key];
};
getItemIDs = function() {
return Object.keys(itemByKey).map(getIDFromKey);
};
addRoot = function(id) {
var key = getKeyFromID(id);
rootByKey[key] = true;
};
removeRoot = function(id) {
var key = getKeyFromID(id);
delete rootByKey[key];
};
getRootIDs = function() {
return Object.keys(rootByKey).map(getIDFromKey);
};
}
var unmountedIDs: Array<DebugID> = [];
function purgeDeep(id) {
var item = getItem(id);
if (item) {
var {childIDs} = item;
removeItem(id);
childIDs.forEach(purgeDeep);
}
}
function describeComponentFrame(name, source, ownerName) {
return '\n in ' + (name || 'Unknown') + (
source ?
@@ -170,35 +36,6 @@ function describeComponentFrame(name, source, ownerName) {
);
}
function getDisplayName(element: ?ReactElement): string {
if (element == null) {
return '#empty';
} else if (typeof element === 'string' || typeof element === 'number') {
return '#text';
} else if (typeof element.type === 'string') {
return element.type;
} else {
return element.type.displayName || element.type.name || 'Unknown';
}
}
function describeID(id: DebugID): string {
var name = ReactComponentTreeHook.getDisplayName(id);
var element = ReactComponentTreeHook.getElement(id);
var ownerID = ReactComponentTreeHook.getOwnerID(id);
var ownerName;
if (ownerID) {
ownerName = ReactComponentTreeHook.getDisplayName(ownerID);
}
warning(
element,
'ReactComponentTreeHook: Missing React element for debugID %s when ' +
'building stack',
id
);
return describeComponentFrame(name, element && element._source, ownerName);
}
function describeFiber(fiber : Fiber) : string {
switch (fiber.tag) {
case IndeterminateComponent:
@@ -219,160 +56,6 @@ function describeFiber(fiber : Fiber) : string {
}
var ReactComponentTreeHook = {
onSetChildren(id: DebugID, nextChildIDs: Array<DebugID>): void {
var item = getItem(id);
invariant(item, 'Item must have been set');
item.childIDs = nextChildIDs;
for (var i = 0; i < nextChildIDs.length; i++) {
var nextChildID = nextChildIDs[i];
var nextChild = getItem(nextChildID);
invariant(
nextChild,
'Expected hook events to fire for the child ' +
'before its parent includes it in onSetChildren().'
);
invariant(
nextChild.childIDs != null ||
typeof nextChild.element !== 'object' ||
nextChild.element == null,
'Expected onSetChildren() to fire for a container child ' +
'before its parent includes it in onSetChildren().'
);
invariant(
nextChild.isMounted,
'Expected onMountComponent() to fire for the child ' +
'before its parent includes it in onSetChildren().'
);
if (nextChild.parentID == null) {
nextChild.parentID = id;
// TODO: This shouldn't be necessary but mounting a new root during in
// componentWillMount currently causes not-yet-mounted components to
// be purged from our tree data so their parent id is missing.
}
invariant(
nextChild.parentID === id,
'Expected onBeforeMountComponent() parent and onSetChildren() to ' +
'be consistent (%s has parents %s and %s).',
nextChildID,
nextChild.parentID,
id
);
}
},
onBeforeMountComponent(id: DebugID, element: ReactElement, parentID: DebugID): void {
var item = {
element,
parentID,
text: null,
childIDs: [],
isMounted: false,
updateCount: 0,
};
setItem(id, item);
},
onBeforeUpdateComponent(id: DebugID, element: ReactElement): void {
var item = getItem(id);
if (!item || !item.isMounted) {
// We may end up here as a result of setState() in componentWillUnmount().
// In this case, ignore the element.
return;
}
item.element = element;
},
onMountComponent(id: DebugID): void {
var item = getItem(id);
invariant(item, 'Item must have been set');
item.isMounted = true;
var isRoot = item.parentID === 0;
if (isRoot) {
addRoot(id);
}
},
onUpdateComponent(id: DebugID): void {
var item = getItem(id);
if (!item || !item.isMounted) {
// We may end up here as a result of setState() in componentWillUnmount().
// In this case, ignore the element.
return;
}
item.updateCount++;
},
onUnmountComponent(id: DebugID): void {
var item = getItem(id);
if (item) {
// We need to check if it exists.
// `item` might not exist if it is inside an error boundary, and a sibling
// error boundary child threw while mounting. Then this instance never
// got a chance to mount, but it still gets an unmounting event during
// the error boundary cleanup.
item.isMounted = false;
var isRoot = item.parentID === 0;
if (isRoot) {
removeRoot(id);
}
}
unmountedIDs.push(id);
},
purgeUnmountedComponents(): void {
if (ReactComponentTreeHook._preventPurging) {
// Should only be used for testing.
return;
}
for (var i = 0; i < unmountedIDs.length; i++) {
var id = unmountedIDs[i];
purgeDeep(id);
}
unmountedIDs.length = 0;
},
isMounted(id: DebugID): boolean {
var item = getItem(id);
return item ? item.isMounted : false;
},
getCurrentStackAddendum(topElement: ?ReactElement): string {
var info = '';
if (topElement) {
var name = getDisplayName(topElement);
var owner = topElement._owner;
info += describeComponentFrame(
name,
topElement._source,
owner && getComponentName(owner)
);
}
var currentOwner = ReactCurrentOwner.current;
if (currentOwner) {
if (typeof currentOwner.tag === 'number') {
const workInProgress = ((currentOwner : any) : Fiber);
// Safe because if current owner exists, we are reconciling,
// and it is guaranteed to be the work-in-progress version.
info += ReactComponentTreeHook.getStackAddendumByWorkInProgressFiber(workInProgress);
} else if (typeof currentOwner._debugID === 'number') {
info += ReactComponentTreeHook.getStackAddendumByID(currentOwner._debugID);
}
}
return info;
},
getStackAddendumByID(id: ?DebugID): string {
var info = '';
while (id) {
info += describeID(id);
id = ReactComponentTreeHook.getParentID(id);
}
return info;
},
// This function can only be called with a work-in-progress fiber and
// only during begin or complete phase. Do not call it under any other
// circumstances.
@@ -386,63 +69,392 @@ var ReactComponentTreeHook = {
} while (node);
return info;
},
};
getChildIDs(id: DebugID): Array<DebugID> {
var item = getItem(id);
return item ? item.childIDs : [];
},
if (__DEV__) {
var ReactCurrentOwner = require('ReactCurrentOwner');
var invariant = require('fbjs/lib/invariant');
var warning = require('fbjs/lib/warning');
getDisplayName(id: DebugID): ?string {
var element = ReactComponentTreeHook.getElement(id);
if (!element) {
return null;
var isNative = function(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;
}
return getDisplayName(element);
},
};
getElement(id: DebugID): ?ReactElement {
var canUseCollections = (
// Array.from
typeof Array.from === 'function' &&
// Map
typeof Map === 'function' &&
isNative(Map) &&
// Map.prototype.keys
Map.prototype != null &&
typeof Map.prototype.keys === 'function' &&
isNative(Map.prototype.keys) &&
// Set
typeof Set === 'function' &&
isNative(Set) &&
// Set.prototype.keys
Set.prototype != null &&
typeof Set.prototype.keys === 'function' &&
isNative(Set.prototype.keys)
);
var setItem;
var getItem;
var removeItem;
var getItemIDs;
var addRoot;
var removeRoot;
var getRootIDs;
if (canUseCollections) {
var itemMap = new Map();
var rootIDSet = new Set();
setItem = function(id, item) {
itemMap.set(id, item);
};
getItem = function(id) {
return itemMap.get(id);
};
removeItem = function(id) {
itemMap.delete(id);
};
getItemIDs = function() {
return Array.from(itemMap.keys());
};
addRoot = function(id) {
rootIDSet.add(id);
};
removeRoot = function(id) {
rootIDSet.delete(id);
};
getRootIDs = function() {
return Array.from(rootIDSet.keys());
};
} else {
var itemByKey = {};
var rootByKey = {};
// Use non-numeric keys to prevent V8 performance issues:
// https://github.com/facebook/react/pull/7232
var getKeyFromID = function(id: DebugID): string {
return '.' + id;
};
var getIDFromKey = function(key: string): DebugID {
return parseInt(key.substr(1), 10);
};
setItem = function(id, item) {
var key = getKeyFromID(id);
itemByKey[key] = item;
};
getItem = function(id) {
var key = getKeyFromID(id);
return itemByKey[key];
};
removeItem = function(id) {
var key = getKeyFromID(id);
delete itemByKey[key];
};
getItemIDs = function() {
return Object.keys(itemByKey).map(getIDFromKey);
};
addRoot = function(id) {
var key = getKeyFromID(id);
rootByKey[key] = true;
};
removeRoot = function(id) {
var key = getKeyFromID(id);
delete rootByKey[key];
};
getRootIDs = function() {
return Object.keys(rootByKey).map(getIDFromKey);
};
}
var unmountedIDs: Array<DebugID> = [];
var purgeDeep = function(id) {
var item = getItem(id);
return item ? item.element : null;
},
if (item) {
var {childIDs} = item;
removeItem(id);
childIDs.forEach(purgeDeep);
}
};
getOwnerID(id: DebugID): ?DebugID {
var element = ReactComponentTreeHook.getElement(id);
var getDisplayName = function(element: ?ReactElement): string {
if (element == null) {
return '#empty';
} else if (typeof element === 'string' || typeof element === 'number') {
return '#text';
} else if (typeof element.type === 'string') {
return element.type;
} else {
return element.type.displayName || element.type.name || 'Unknown';
}
};
var describeID = function(id: DebugID): string {
var name = getDisplayName((id: any));
var element = getElement(id);
var ownerID: any = getOwnerID(id);
var ownerName;
if (ownerID) {
ownerName = getDisplayName(ownerID);
}
warning(
element,
'ReactComponentTreeHook: Missing React element for debugID %s when ' +
'building stack',
id
);
return describeComponentFrame(name, element && element._source, ownerName);
};
var getOwnerID = function(id: DebugID): ?DebugID {
var element = getElement(id);
if (!element || !element._owner) {
return null;
}
return element._owner._debugID;
},
};
getParentID(id: DebugID): ?DebugID {
var getElement = function(id: DebugID): ?ReactElement {
var item = getItem(id);
return item ? item.element : null;
};
var getParentID = function(id: DebugID): ?DebugID {
var item = getItem(id);
return item ? item.parentID : null;
},
getSource(id: DebugID): ?Source {
var item = getItem(id);
var element = item ? item.element : null;
var source = element != null ? element._source : null;
return source;
},
getText(id: DebugID): ?string {
var element = ReactComponentTreeHook.getElement(id);
if (typeof element === 'string') {
return element;
} else if (typeof element === 'number') {
return '' + element;
} else {
return null;
};
var getStackAddendumByID = function(id: ?DebugID): string {
var info = '';
while (id) {
info += describeID(id);
id = getParentID(id);
}
},
return info;
};
getUpdateCount(id: DebugID): number {
var item = getItem(id);
return item ? item.updateCount : 0;
},
ReactComponentTreeHook = Object.assign({}, ReactComponentTreeHook, {
onSetChildren(id: DebugID, nextChildIDs: Array<DebugID>): void {
var item = getItem(id);
invariant(item, 'Item must have been set');
item.childIDs = nextChildIDs;
getRootIDs,
getRegisteredIDs: getItemIDs,
};
for (var i = 0; i < nextChildIDs.length; i++) {
var nextChildID = nextChildIDs[i];
var nextChild = getItem(nextChildID);
invariant(
nextChild,
'Expected hook events to fire for the child ' +
'before its parent includes it in onSetChildren().'
);
invariant(
nextChild.childIDs != null ||
typeof nextChild.element !== 'object' ||
nextChild.element == null,
'Expected onSetChildren() to fire for a container child ' +
'before its parent includes it in onSetChildren().'
);
invariant(
nextChild.isMounted,
'Expected onMountComponent() to fire for the child ' +
'before its parent includes it in onSetChildren().'
);
if (nextChild.parentID == null) {
nextChild.parentID = id;
// TODO: This shouldn't be necessary but mounting a new root during in
// componentWillMount currently causes not-yet-mounted components to
// be purged from our tree data so their parent id is missing.
}
invariant(
nextChild.parentID === id,
'Expected onBeforeMountComponent() parent and onSetChildren() to ' +
'be consistent (%s has parents %s and %s).',
nextChildID,
nextChild.parentID,
id
);
}
},
onBeforeMountComponent(id: DebugID, element: ReactElement, parentID: DebugID): void {
var item = {
element,
parentID,
text: null,
childIDs: [],
isMounted: false,
updateCount: 0,
};
setItem(id, item);
},
onBeforeUpdateComponent(id: DebugID, element: ReactElement): void {
var item = getItem(id);
if (!item || !item.isMounted) {
// We may end up here as a result of setState() in componentWillUnmount().
// In this case, ignore the element.
return;
}
item.element = element;
},
onMountComponent(id: DebugID): void {
var item = getItem(id);
invariant(item, 'Item must have been set');
item.isMounted = true;
var isRoot = item.parentID === 0;
if (isRoot) {
addRoot(id);
}
},
onUpdateComponent(id: DebugID): void {
var item = getItem(id);
if (!item || !item.isMounted) {
// We may end up here as a result of setState() in componentWillUnmount().
// In this case, ignore the element.
return;
}
item.updateCount++;
},
onUnmountComponent(id: DebugID): void {
var item = getItem(id);
if (item) {
// We need to check if it exists.
// `item` might not exist if it is inside an error boundary, and a sibling
// error boundary child threw while mounting. Then this instance never
// got a chance to mount, but it still gets an unmounting event during
// the error boundary cleanup.
item.isMounted = false;
var isRoot = item.parentID === 0;
if (isRoot) {
removeRoot(id);
}
}
unmountedIDs.push(id);
},
purgeUnmountedComponents(): void {
if (ReactComponentTreeHook._preventPurging) {
// Should only be used for testing.
return;
}
for (var i = 0; i < unmountedIDs.length; i++) {
var id = unmountedIDs[i];
purgeDeep(id);
}
unmountedIDs.length = 0;
},
isMounted(id: DebugID): boolean {
var item = getItem(id);
return item ? item.isMounted : false;
},
getCurrentStackAddendum(topElement: ?ReactElement): string {
var info = '';
if (topElement) {
var name = getDisplayName(topElement);
var owner = topElement._owner;
info += describeComponentFrame(
name,
topElement._source,
owner && getComponentName(owner)
);
}
var currentOwner = ReactCurrentOwner.current;
if (currentOwner) {
if (typeof currentOwner.tag === 'number') {
const workInProgress = ((currentOwner : any) : Fiber);
// Safe because if current owner exists, we are reconciling,
// and it is guaranteed to be the work-in-progress version.
info += ReactComponentTreeHook.getStackAddendumByWorkInProgressFiber(workInProgress);
} else if (typeof currentOwner._debugID === 'number') {
info += getStackAddendumByID(currentOwner._debugID);
}
}
return info;
},
getStackAddendumByID,
getChildIDs(id: DebugID): Array<DebugID> {
var item = getItem(id);
return item ? item.childIDs : [];
},
getDisplayName(id: DebugID): ?string {
var element = getElement(id);
if (!element) {
return null;
}
return getDisplayName(element);
},
getElement,
getOwnerID,
getParentID,
getSource(id: DebugID): ?Source {
var item = getItem(id);
var element = item ? item.element : null;
var source = element != null ? element._source : null;
return source;
},
getText(id: DebugID): ?string {
var element = getElement(id);
if (typeof element === 'string') {
return element;
} else if (typeof element === 'number') {
return '' + element;
} else {
return null;
}
},
getUpdateCount(id: DebugID): number {
var item = getItem(id);
return item ? item.updateCount : 0;
},
getRootIDs,
getRegisteredIDs: getItemIDs,
});
}
module.exports = ReactComponentTreeHook;
+11 -11
View File
@@ -108,22 +108,22 @@ if (__DEV__) {
var lifeCycleTimerHasWarned = false;
const clearHistory = function() {
ReactComponentTreeHook.purgeUnmountedComponents();
(ReactComponentTreeHook: any).purgeUnmountedComponents();
ReactHostOperationHistoryHook.clearHistory();
};
const getTreeSnapshot = function(registeredIDs) {
return registeredIDs.reduce((tree, id) => {
var ownerID = ReactComponentTreeHook.getOwnerID(id);
var parentID = ReactComponentTreeHook.getParentID(id);
var ownerID = (ReactComponentTreeHook: any).getOwnerID(id);
var parentID = (ReactComponentTreeHook: any).getParentID(id);
tree[id] = {
displayName: ReactComponentTreeHook.getDisplayName(id),
text: ReactComponentTreeHook.getText(id),
updateCount: ReactComponentTreeHook.getUpdateCount(id),
childIDs: ReactComponentTreeHook.getChildIDs(id),
displayName: (ReactComponentTreeHook: any).getDisplayName(id),
text: (ReactComponentTreeHook: any).getText(id),
updateCount: (ReactComponentTreeHook: any).getUpdateCount(id),
childIDs: (ReactComponentTreeHook: any).getChildIDs(id),
// Text nodes don't have owners but this is close enough.
ownerID: ownerID ||
parentID && ReactComponentTreeHook.getOwnerID(parentID) ||
parentID && (ReactComponentTreeHook: any).getOwnerID(parentID) ||
0,
parentID,
};
@@ -144,7 +144,7 @@ if (__DEV__) {
}
if (previousMeasurements.length || previousOperations.length) {
var registeredIDs = ReactComponentTreeHook.getRegisteredIDs();
var registeredIDs = (ReactComponentTreeHook: any).getRegisteredIDs();
flushHistory.push({
duration: performanceNow() - previousStartTime,
measurements: previousMeasurements || [],
@@ -253,7 +253,7 @@ if (__DEV__) {
if (!isProfiling || !canUsePerformanceMeasure) {
return false;
}
var element = ReactComponentTreeHook.getElement(debugID);
var element = (ReactComponentTreeHook: any).getElement(debugID);
if (element == null || typeof element !== 'object') {
return false;
}
@@ -280,7 +280,7 @@ if (__DEV__) {
}
var markName = `${debugID}::${markType}`;
var displayName = ReactComponentTreeHook.getDisplayName(debugID) || 'Unknown';
var displayName = (ReactComponentTreeHook: any).getDisplayName(debugID) || 'Unknown';
// Chrome has an issue of dropping markers recorded too fast:
// https://bugs.chromium.org/p/chromium/issues/detail?id=640652
@@ -53,7 +53,7 @@ function attachRef(ref, component, owner) {
'Stateless function components cannot be given refs. ' +
'Attempts to access this ref will fail.%s%s',
info,
ReactComponentTreeHook.getStackAddendumByID(component._debugID)
(ReactComponentTreeHook: any).getStackAddendumByID(component._debugID)
);
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ function flattenSingleChildIntoContext(
'`%s`. Child keys must be unique; when two children share a key, only ' +
'the first child will be used.%s',
KeyEscapeUtils.unescape(name),
ReactComponentTreeHook.getStackAddendumByID(selfDebugID)
(ReactComponentTreeHook: any).getStackAddendumByID(selfDebugID)
);
}
}