mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Replaced node objects with typed array of tree operations; windowing works in small test harness
This commit is contained in:
+1
-1
@@ -90,7 +90,7 @@
|
||||
"react-dom": "^16.8.0-alpha.1",
|
||||
"react-portal": "^3.1.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.5.0",
|
||||
"react-window": "^1.5.1",
|
||||
"semver": "^5.5.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"web-ext": "^1.10.1",
|
||||
|
||||
+22
-163
@@ -1,44 +1,28 @@
|
||||
// @flow
|
||||
|
||||
import nullthrows from 'nullthrows';
|
||||
import EventEmitter from 'events';
|
||||
import { guid } from './utils';
|
||||
import { ElementTypeOtherOrUnknown } from 'src/devtools/types';
|
||||
|
||||
import type {
|
||||
Fiber,
|
||||
RendererData,
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { RendererID, RendererInterface } from './types';
|
||||
import type { Bridge } from '../types';
|
||||
import type { Element } from 'src/devtools/types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
console.log(`%cAgent %c${methodName}`, 'color: blue; font-weight: bold;', 'font-weight: bold;', ...args);
|
||||
console.log(
|
||||
`%cAgent %c${methodName}`,
|
||||
'color: purple; font-weight: bold;',
|
||||
'font-weight: bold;',
|
||||
...args
|
||||
);
|
||||
};
|
||||
|
||||
const THROTTLE_BY_MS = 350;
|
||||
|
||||
export default class Agent extends EventEmitter {
|
||||
_fiberToID: WeakMap<Fiber, string> = new WeakMap();
|
||||
_idToElement: Map<string, Element> = new Map();
|
||||
_idToFiber: Map<string, Fiber> = new Map();
|
||||
_idToRendererData: Map<string, RendererData> = new Map();
|
||||
_idToRendererID: Map<string, RendererID> = new Map();
|
||||
_bridge: Bridge = ((null: any): Bridge);
|
||||
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
|
||||
_roots: Set<RendererID> = new Set();
|
||||
|
||||
addBridge(bridge: Bridge) {
|
||||
this._bridge = bridge;
|
||||
|
||||
// TODO Listen to bridge for things like selection.
|
||||
// bridge.on('...'), this...);
|
||||
|
||||
this.addListener('root', id => bridge.send('root', id));
|
||||
this.addListener('rootCommitted', id => bridge.send('rootCommitted', id));
|
||||
this.addListener('mount', data => bridge.send('mount', data));
|
||||
this.addListener('update', data => bridge.send('update', data));
|
||||
this.addListener('unmount', data => bridge.send('unmount', data));
|
||||
// TODO Add other methods for e.g. profiling.
|
||||
}
|
||||
|
||||
setRendererInterface(
|
||||
@@ -48,148 +32,23 @@ export default class Agent extends EventEmitter {
|
||||
this._rendererInterfaces[rendererID] = rendererInterface;
|
||||
}
|
||||
|
||||
_getId(fiber: Fiber): string {
|
||||
if (typeof fiber !== 'object' || !fiber) {
|
||||
return fiber;
|
||||
}
|
||||
if (!this._fiberToID.has(fiber)) {
|
||||
this._fiberToID.set(fiber, guid());
|
||||
this._idToFiber.set(nullthrows(this._fiberToID.get(fiber)), fiber);
|
||||
}
|
||||
return nullthrows(this._fiberToID.get(fiber));
|
||||
}
|
||||
|
||||
_crawl(parent: Element, id: string): void {
|
||||
const data: RendererData = ((this._idToRendererData.get(
|
||||
id
|
||||
): any): RendererData);
|
||||
if (data.type === ElementTypeOtherOrUnknown) {
|
||||
data.children.forEach(childFiber => {
|
||||
if (childFiber !== null) {
|
||||
this._crawl(parent, this._getId(childFiber));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
parent.children.push(id);
|
||||
|
||||
this._createOrUpdateElement(id, data);
|
||||
}
|
||||
}
|
||||
|
||||
_createOrUpdateElement(id: string, data: RendererData): void {
|
||||
const prevElement: ?Element = this._idToElement.get(id);
|
||||
const nextElement: Element = {
|
||||
id,
|
||||
key: data.key,
|
||||
displayName: data.displayName,
|
||||
children: [],
|
||||
type: data.type,
|
||||
};
|
||||
|
||||
this._idToElement.set(id, nextElement);
|
||||
|
||||
data.children.forEach(childFiber => {
|
||||
if (childFiber !== null) {
|
||||
this._crawl(nextElement, this._getId(childFiber));
|
||||
}
|
||||
});
|
||||
|
||||
if (prevElement == null) {
|
||||
debug('emit("mount")', id, nextElement);
|
||||
this.emit('mount', nextElement);
|
||||
} else if (!areElementsEqual(prevElement, nextElement)) {
|
||||
debug('emit("update")', id, nextElement);
|
||||
this.emit('update', nextElement);
|
||||
}
|
||||
}
|
||||
|
||||
onHookMount = ({
|
||||
data,
|
||||
fiber,
|
||||
renderer,
|
||||
}: {
|
||||
data: RendererData,
|
||||
fiber: Fiber,
|
||||
renderer: RendererID,
|
||||
}) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
this._idToRendererData.set(id, data);
|
||||
this._idToRendererID.set(id, renderer);
|
||||
onHookDisplayNames = (displayNames: Map<number, string>) => {
|
||||
debug('onHookDisplayNames', displayNames);
|
||||
this._bridge.send('displayNames', displayNames);
|
||||
};
|
||||
|
||||
onHookRootCommitted = ({
|
||||
data,
|
||||
fiber,
|
||||
renderer,
|
||||
}: {
|
||||
data: RendererData,
|
||||
fiber: Fiber,
|
||||
renderer: RendererID,
|
||||
}) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
// TODO: Can we use the effects list on update for a faster path?
|
||||
// Mounts (Placements) and unmounts (Deletions) are on the child; need to update parent children in that case.
|
||||
this._createOrUpdateElement(id, data);
|
||||
|
||||
if (!this._roots.has(id)) {
|
||||
this._roots.add(id);
|
||||
debug('emit("root")', id);
|
||||
this.emit('root', id);
|
||||
}
|
||||
|
||||
this.emit('rootCommitted', id);
|
||||
onHookKeys = (keys: Map<number, string>) => {
|
||||
debug('onHookKeys', keys);
|
||||
this._bridge.send('keys', keys);
|
||||
};
|
||||
|
||||
onHookUnmount = ({ fiber }: { fiber: Fiber }) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
if (this._roots.has(id)) {
|
||||
this._roots.delete(id);
|
||||
this.emit('rootUnmounted', id);
|
||||
}
|
||||
|
||||
if (this._idToElement.has(id)) {
|
||||
this._idToElement.delete(id);
|
||||
debug('emit("unmount")', id);
|
||||
this.emit('unmount', id);
|
||||
}
|
||||
|
||||
this._fiberToID.delete(fiber);
|
||||
this._idToRendererData.delete(id);
|
||||
this._idToRendererID.delete(id);
|
||||
onHookOperations = (operations: Uint32Array) => {
|
||||
debug('onHookOperations', operations);
|
||||
this._bridge.send('operations', operations);
|
||||
};
|
||||
|
||||
onHookUpdate = ({ data, fiber }: { data: RendererData, fiber: Fiber }) => {
|
||||
const id = this._getId(fiber);
|
||||
|
||||
this._idToRendererData.set(id, data);
|
||||
onHookRootCommitted = (rootID: string) => {
|
||||
debug('onHookRootCommitted', rootID);
|
||||
this._bridge.send('rootCommitted', rootID);
|
||||
};
|
||||
}
|
||||
|
||||
function areElementsEqual(
|
||||
prevElement: ?Element,
|
||||
nextElement: ?Element
|
||||
): boolean {
|
||||
if (!prevElement || !nextElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevElement.key !== nextElement.key ||
|
||||
prevElement.displayName !== nextElement.displayName ||
|
||||
prevElement.children.length !== nextElement.children.length ||
|
||||
prevElement.type !== nextElement.type
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < prevElement.children.length; i++) {
|
||||
if (prevElement.children[i] !== nextElement.children[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,8 @@ export function initBackend(hook: Hook, agent: Agent): void {
|
||||
rendererInterface.walkTree();
|
||||
}),
|
||||
|
||||
hook.sub('operations', agent.onHookOperations),
|
||||
hook.sub('rootCommitted', agent.onHookRootCommitted),
|
||||
hook.sub('mount', agent.onHookMount),
|
||||
hook.sub('update', agent.onHookUpdate),
|
||||
hook.sub('unmount', agent.onHookUnmount),
|
||||
|
||||
// TODO Add additional subscriptions required for profiling mode
|
||||
];
|
||||
|
||||
+263
-174
@@ -8,21 +8,25 @@ import {
|
||||
ElementTypeMemo,
|
||||
ElementTypeOtherOrUnknown,
|
||||
ElementTypeProfiler,
|
||||
ElementTypeRoot,
|
||||
ElementTypeSuspense,
|
||||
} from 'src/devtools/types';
|
||||
import { utfEncodeString } from '../utils';
|
||||
import { getDisplayName } from './utils';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_RESET_CHILDREN,
|
||||
} from '../constants';
|
||||
|
||||
import type {
|
||||
Fiber,
|
||||
Hook,
|
||||
ReactRenderer,
|
||||
RendererData,
|
||||
FiberData,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
|
||||
// TODO: If we're profiling, process update
|
||||
// TODO: Throttle process update to app tree
|
||||
|
||||
function getInternalReactConstants(version) {
|
||||
const ReactSymbols = {
|
||||
CONCURRENT_MODE_NUMBER: 0xeacf,
|
||||
@@ -184,10 +188,61 @@ export function attach(
|
||||
|
||||
const primaryFibers: WeakSet<Fiber> = new WeakSet();
|
||||
|
||||
// TODO: we might want to change the data structure
|
||||
// once we no longer suppport Stack versions of `getData`.
|
||||
// Keep this function in sync with getDataForFiber()
|
||||
function shouldFilterFiber(fiber: Fiber): boolean {
|
||||
const { type, tag } = fiber;
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case FunctionComponent:
|
||||
case IncompleteClassComponent:
|
||||
case IndeterminateComponent:
|
||||
case ForwardRef:
|
||||
case HostRoot:
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
return false;
|
||||
case HostPortal:
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
return true;
|
||||
default:
|
||||
const symbolOrNumber =
|
||||
typeof type === 'object' && type !== null ? type.$$typeof : type;
|
||||
|
||||
const switchValue =
|
||||
// $FlowFixMe facebook/flow/issues/2362
|
||||
typeof symbolOrNumber === 'symbol'
|
||||
? symbolOrNumber.toString()
|
||||
: symbolOrNumber;
|
||||
|
||||
switch (switchValue) {
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
return true;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_PROVIDER_SYMBOL_STRING:
|
||||
case CONTEXT_CONSUMER_NUMBER:
|
||||
case CONTEXT_CONSUMER_SYMBOL_STRING:
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we might want to change the data structure once we no longer suppport Stack versions of `getData`.
|
||||
// TODO: Keep in sync with getElementType()
|
||||
function getRendererDataFromFiber(fiber: Fiber): RendererData {
|
||||
function getDataForFiber(fiber: Fiber): FiberData {
|
||||
const { elementType, type, key, tag } = fiber;
|
||||
|
||||
// This is to support lazy components with a Promise as the type.
|
||||
@@ -199,11 +254,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
// Suspense has some special behavior when timed out that we have to handle.
|
||||
// see https://github.com/facebook/react/pull/13823
|
||||
let isTimedOutSuspense: boolean = false;
|
||||
|
||||
let rendererData: RendererData = ((null: any): RendererData);
|
||||
let fiberData: FiberData = ((null: any): FiberData);
|
||||
let displayName: string = ((null: any): string);
|
||||
let resolvedContext: any = null;
|
||||
|
||||
@@ -212,8 +263,7 @@ export function attach(
|
||||
case FunctionComponent:
|
||||
case IncompleteClassComponent:
|
||||
case IndeterminateComponent:
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName: getDisplayName(resolvedType),
|
||||
key,
|
||||
type: ElementTypeClassOrFunction,
|
||||
@@ -224,26 +274,28 @@ export function attach(
|
||||
displayName =
|
||||
resolvedType.displayName ||
|
||||
(functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef');
|
||||
rendererData = {
|
||||
children: [],
|
||||
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeForwardRef,
|
||||
};
|
||||
break;
|
||||
case HostRoot:
|
||||
return {
|
||||
displayName: null,
|
||||
key: null,
|
||||
type: ElementTypeRoot,
|
||||
};
|
||||
case HostPortal:
|
||||
case HostComponent:
|
||||
case HostText:
|
||||
case Fragment:
|
||||
// Not displayed in Elements tree
|
||||
rendererData = {
|
||||
children: [],
|
||||
return {
|
||||
displayName: null,
|
||||
key,
|
||||
key: null,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
case MemoComponent:
|
||||
case SimpleMemoComponent:
|
||||
if (elementType.displayName) {
|
||||
@@ -252,8 +304,7 @@ export function attach(
|
||||
displayName = type.displayName || type.name;
|
||||
displayName = displayName ? `Memo(${displayName})` : 'Memo';
|
||||
}
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeMemo,
|
||||
@@ -273,14 +324,11 @@ export function attach(
|
||||
case CONCURRENT_MODE_NUMBER:
|
||||
case CONCURRENT_MODE_SYMBOL_STRING:
|
||||
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
||||
// Not displayed in Elements tree
|
||||
rendererData = {
|
||||
children: [],
|
||||
return {
|
||||
displayName: null,
|
||||
key,
|
||||
key: null,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
};
|
||||
break;
|
||||
case CONTEXT_PROVIDER_NUMBER:
|
||||
case CONTEXT_PROVIDER_SYMBOL_STRING:
|
||||
// 16.3.0 exposed the context object as "context"
|
||||
@@ -289,8 +337,7 @@ export function attach(
|
||||
displayName = `${resolvedContext.displayName ||
|
||||
'Context'}.Provider`;
|
||||
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeContext,
|
||||
@@ -307,8 +354,7 @@ export function attach(
|
||||
displayName = `${resolvedContext.displayName ||
|
||||
'Context'}.Consumer`;
|
||||
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName,
|
||||
key,
|
||||
type: ElementTypeContext,
|
||||
@@ -316,8 +362,7 @@ export function attach(
|
||||
break;
|
||||
case STRICT_MODE_NUMBER:
|
||||
case STRICT_MODE_SYMBOL_STRING:
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
@@ -326,11 +371,7 @@ export function attach(
|
||||
case SUSPENSE_NUMBER:
|
||||
case SUSPENSE_SYMBOL_STRING:
|
||||
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
|
||||
// Suspense components only have a non-null memoizedState if they're timed-out.
|
||||
isTimedOutSuspense = fiber.memoizedState !== null;
|
||||
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName: 'Suspense',
|
||||
key,
|
||||
type: ElementTypeSuspense,
|
||||
@@ -338,8 +379,7 @@ export function attach(
|
||||
break;
|
||||
case PROFILER_NUMBER:
|
||||
case PROFILER_SYMBOL_STRING:
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName: `Profiler(${fiber.memoizedProps.id})`,
|
||||
key,
|
||||
type: ElementTypeProfiler,
|
||||
@@ -348,8 +388,7 @@ export function attach(
|
||||
default:
|
||||
// Unknown element type.
|
||||
// This may mean a new element type that has not yet been added to DevTools.
|
||||
rendererData = {
|
||||
children: [],
|
||||
fiberData = {
|
||||
displayName: null,
|
||||
key,
|
||||
type: ElementTypeOtherOrUnknown,
|
||||
@@ -359,30 +398,7 @@ export function attach(
|
||||
break;
|
||||
}
|
||||
|
||||
const { children } = rendererData;
|
||||
if (isTimedOutSuspense) {
|
||||
// The behavior of timed-out Suspense trees is unique.
|
||||
// Rather than unmount the timed out content (and possibly lose important state),
|
||||
// React re-parents this content within a hidden Fragment while the fallback is showing.
|
||||
// This behavior doesn't need to be observable in the DevTools though.
|
||||
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
|
||||
// The easiest fix is to strip out the intermediate Fragment fibers,
|
||||
// so the Elements panel and Profiler don't need to special case them.
|
||||
const primaryChildFragment = fiber.child;
|
||||
const primaryChild = primaryChildFragment.child;
|
||||
const fallbackChildFragment = primaryChildFragment.sibling;
|
||||
const fallbackChild = fallbackChildFragment.child;
|
||||
children.push(primaryChild);
|
||||
children.push(fallbackChild);
|
||||
} else {
|
||||
let child = fiber.child;
|
||||
while (child) {
|
||||
children.push(getPrimaryFiber(child));
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
|
||||
return rendererData;
|
||||
return fiberData;
|
||||
}
|
||||
|
||||
// This is a slightly annoying indirection.
|
||||
@@ -401,6 +417,17 @@ export function attach(
|
||||
return fiber;
|
||||
}
|
||||
|
||||
let uidCounter: number = 0;
|
||||
const fiberToIDMap: WeakMap<Fiber, number> = new WeakMap();
|
||||
|
||||
function getFiberID(primaryFiber: Fiber): number {
|
||||
if (!fiberToIDMap.has(primaryFiber)) {
|
||||
fiberToIDMap.set(primaryFiber, ++uidCounter);
|
||||
}
|
||||
return ((fiberToIDMap.get(primaryFiber): any): number);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
|
||||
switch (nextFiber.tag) {
|
||||
case ClassComponent:
|
||||
@@ -425,6 +452,7 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function haveProfilerTimesChanged(
|
||||
prevFiber: Fiber,
|
||||
nextFiber: Fiber
|
||||
@@ -437,127 +465,176 @@ export function attach(
|
||||
);
|
||||
}
|
||||
|
||||
let pendingEvents = [];
|
||||
let pendingOperations: Uint32Array = new Uint32Array(0);
|
||||
|
||||
// TODO: Do we still need to send events like this?
|
||||
function flushPendingEvents() {
|
||||
const events = pendingEvents;
|
||||
pendingEvents = [];
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const event = events[i];
|
||||
hook.emit(event.type, event);
|
||||
function addOperation(
|
||||
newAction: Uint32Array,
|
||||
addToStartOfQueue: boolean = false
|
||||
): void {
|
||||
const oldActions = pendingOperations;
|
||||
pendingOperations = new Uint32Array(oldActions.length + newAction.length);
|
||||
if (addToStartOfQueue) {
|
||||
pendingOperations.set(newAction);
|
||||
pendingOperations.set(oldActions, newAction.length);
|
||||
} else {
|
||||
pendingOperations.set(oldActions);
|
||||
pendingOperations.set(newAction, oldActions.length);
|
||||
}
|
||||
}
|
||||
|
||||
function enqueueMount(fiber) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data: getRendererDataFromFiber(fiber),
|
||||
renderer: rendererID,
|
||||
type: 'mount',
|
||||
});
|
||||
function flushPendingEvents(root: Object): void {
|
||||
// Let the frontend know about tree operations.
|
||||
hook.emit('operations', pendingOperations);
|
||||
pendingOperations = new Uint32Array(0);
|
||||
|
||||
// Let the frontend know that we're done working on this root.
|
||||
// Technically this could be inferred, but it's better to explicitly do this for the case of multi roots.
|
||||
// Else the frontend would need to traverse the tree to identify which updates corresponded to which roots.
|
||||
hook.emit('rootCommitted', getFiberID(getPrimaryFiber(root.current)));
|
||||
}
|
||||
|
||||
function enqueueMount(fiber: Fiber, parentFiber: Fiber | null) {
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
const id = getFiberID(getPrimaryFiber(fiber));
|
||||
|
||||
if (isRoot) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
renderer: rendererID,
|
||||
type: 'root',
|
||||
});
|
||||
}
|
||||
}
|
||||
const operation = new Uint32Array(4);
|
||||
operation[0] = TREE_OPERATION_ADD;
|
||||
operation[1] = id;
|
||||
operation[2] = ElementTypeRoot;
|
||||
operation[3] = 0; // Identifies this fiber as a root
|
||||
addOperation(operation);
|
||||
} else {
|
||||
const { displayName, key, type } = getDataForFiber(fiber);
|
||||
|
||||
function enqueueUpdateIfNecessary(fiber, hasChildOrderChanged) {
|
||||
const data = getRendererDataFromFiber(fiber);
|
||||
let encodedDisplayName = ((null: any): Uint8Array);
|
||||
let encodedKey = ((null: any): Uint8Array);
|
||||
|
||||
if (!hasChildOrderChanged && !hasDataChanged(fiber.alternate, fiber)) {
|
||||
// If only timing information has changed, we still need to update the nodes.
|
||||
// But we can do it in a faster way since we know it's safe to skip the children.
|
||||
// It's also important to avoid emitting an "update" signal for the node in this case,
|
||||
// Since that would indicate to the Profiler that it was part of the "commit" when it wasn't.
|
||||
if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data,
|
||||
renderer: rendererID,
|
||||
type: 'updateProfileTimes',
|
||||
});
|
||||
if (displayName !== null) {
|
||||
encodedDisplayName = utfEncodeString(displayName);
|
||||
}
|
||||
return;
|
||||
|
||||
if (key !== null) {
|
||||
if (typeof key === 'number') {
|
||||
encodedKey = new Uint8Array(1);
|
||||
encodedKey[0] = key;
|
||||
} else {
|
||||
encodedKey = utfEncodeString(key);
|
||||
}
|
||||
}
|
||||
|
||||
const encodedDisplayNameSize =
|
||||
displayName === null ? 0 : encodedDisplayName.length;
|
||||
const encodedKeySize = key === null ? 0 : encodedKey.length;
|
||||
|
||||
const operation = new Uint32Array(
|
||||
6 + encodedDisplayNameSize + encodedKeySize
|
||||
);
|
||||
operation[0] = TREE_OPERATION_ADD;
|
||||
operation[1] = id;
|
||||
operation[2] = type;
|
||||
operation[3] = getFiberID(getPrimaryFiber(parentFiber));
|
||||
operation[4] = encodedDisplayNameSize;
|
||||
if (displayName !== null) {
|
||||
operation.set(encodedDisplayName, 5);
|
||||
}
|
||||
operation[5 + encodedDisplayNameSize] = encodedKeySize;
|
||||
if (key !== null) {
|
||||
operation.set(encodedKey, 5 + encodedDisplayNameSize + 1);
|
||||
}
|
||||
addOperation(operation);
|
||||
}
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data,
|
||||
renderer: rendererID,
|
||||
type: 'update',
|
||||
});
|
||||
}
|
||||
|
||||
function enqueueUnmount(fiber) {
|
||||
const isRoot = fiber.tag === HostRoot;
|
||||
const primaryFiber = getPrimaryFiber(fiber);
|
||||
const event = {
|
||||
fiber: primaryFiber,
|
||||
renderer: rendererID,
|
||||
type: 'unmount',
|
||||
};
|
||||
if (isRoot) {
|
||||
pendingEvents.push(event);
|
||||
} else {
|
||||
const id = getFiberID(getPrimaryFiber(fiber));
|
||||
const operation = new Uint32Array(2);
|
||||
operation[0] = TREE_OPERATION_REMOVE;
|
||||
operation[1] = id;
|
||||
addOperation(operation);
|
||||
} else if (!shouldFilterFiber(fiber)) {
|
||||
// Non-root fibers are deleted during the commit phase.
|
||||
// They are deleted in the child-first order. However
|
||||
// DevTools currently expects deletions to be parent-first.
|
||||
// This is why we unshift deletions rather than push them.
|
||||
pendingEvents.unshift(event);
|
||||
const id = getFiberID(getPrimaryFiber(fiber));
|
||||
const operation = new Uint32Array(2);
|
||||
operation[0] = TREE_OPERATION_REMOVE;
|
||||
operation[1] = id;
|
||||
addOperation(operation, true);
|
||||
}
|
||||
primaryFibers.delete(primaryFiber);
|
||||
fiberToIDMap.delete(primaryFiber);
|
||||
}
|
||||
|
||||
function markRootCommitted(fiber) {
|
||||
pendingEvents.push({
|
||||
fiber: getPrimaryFiber(fiber),
|
||||
data: getRendererDataFromFiber(fiber),
|
||||
renderer: rendererID,
|
||||
type: 'rootCommitted',
|
||||
});
|
||||
}
|
||||
function mountFiber(fiber: Fiber, parentFiber: Fiber | null) {
|
||||
const shouldEnqueueMount = !shouldFilterFiber(fiber);
|
||||
|
||||
function mountFiber(fiber) {
|
||||
// Depth-first.
|
||||
// Logs mounting of children first, parents later.
|
||||
let node = fiber;
|
||||
outer: while (true) {
|
||||
if (node.child) {
|
||||
node.child.return = node;
|
||||
node = node.child;
|
||||
continue;
|
||||
}
|
||||
enqueueMount(node);
|
||||
if (node === fiber) {
|
||||
return;
|
||||
}
|
||||
if (node.sibling) {
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
continue;
|
||||
}
|
||||
while (node.return) {
|
||||
node = node.return;
|
||||
enqueueMount(node);
|
||||
if (node === fiber) {
|
||||
return;
|
||||
}
|
||||
if (node.sibling) {
|
||||
node.sibling.return = node.return;
|
||||
node = node.sibling;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
return;
|
||||
if (shouldEnqueueMount) {
|
||||
enqueueMount(fiber, parentFiber);
|
||||
}
|
||||
|
||||
if (fiber.child !== null) {
|
||||
mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber);
|
||||
}
|
||||
|
||||
if (fiber.sibling) {
|
||||
mountFiber(fiber.sibling, parentFiber);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiber(nextFiber, prevFiber) {
|
||||
function enqueueUpdateIfNecessary(
|
||||
fiber: Fiber,
|
||||
hasChildOrderChanged: boolean
|
||||
) {
|
||||
// The frontend only really cares about the displayName, key, and children.
|
||||
// The first two don't really change, so we are only concerned with the order of children here.
|
||||
// This is trickier than a simple comparison though, since certain types of fibers are filtered.
|
||||
if (hasChildOrderChanged) {
|
||||
const nextChildren: Array<number> = [];
|
||||
|
||||
// This is a naive implimentation that shallowly recurses children.
|
||||
// We might want to revisit this if it proves to be too inefficient.
|
||||
let child = fiber.child;
|
||||
while (child !== null) {
|
||||
findReorderedChildren(child, nextChildren);
|
||||
child = child.sibling;
|
||||
}
|
||||
|
||||
const numChildren = nextChildren.length;
|
||||
const operation = new Uint32Array(3 + numChildren);
|
||||
operation[0] = TREE_OPERATION_RESET_CHILDREN;
|
||||
operation[1] = getFiberID(getPrimaryFiber(fiber));
|
||||
operation[2] = numChildren;
|
||||
operation.set(nextChildren, 3);
|
||||
addOperation(operation);
|
||||
}
|
||||
|
||||
// TODO (profiling) If we're profiling, also check to see if that data has changed.
|
||||
}
|
||||
|
||||
function findReorderedChildren(fiber: Fiber, nextChildren: Array<number>) {
|
||||
if (!shouldFilterFiber(fiber)) {
|
||||
nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
|
||||
} else {
|
||||
let child = fiber.child;
|
||||
while (child !== null) {
|
||||
findReorderedChildren(child, nextChildren);
|
||||
child = child.sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFiber(
|
||||
nextFiber: Fiber,
|
||||
prevFiber: Fiber,
|
||||
parentFiber: Fiber | null
|
||||
) {
|
||||
const shouldEnqueueUpdate = !shouldFilterFiber(nextFiber);
|
||||
|
||||
// Suspense components only have a non-null memoizedState if they're timed-out.
|
||||
const isTimedOutSuspense =
|
||||
nextFiber.tag === ReactTypeOfWork.SuspenseComponent &&
|
||||
@@ -574,15 +651,19 @@ export function attach(
|
||||
const primaryChildFragment = nextFiber.child;
|
||||
const fallbackChildFragment = primaryChildFragment.sibling;
|
||||
const fallbackChild = fallbackChildFragment.child;
|
||||
|
||||
// The primary, hidden child is never actually updated in this case,
|
||||
// so we can skip any updates to its tree.
|
||||
// We only need to track updates to the Fallback UI for now.
|
||||
if (fallbackChild.alternate) {
|
||||
updateFiber(fallbackChild, fallbackChild.alternate);
|
||||
updateFiber(fallbackChild, fallbackChild.alternate, nextFiber);
|
||||
} else {
|
||||
mountFiber(fallbackChild);
|
||||
mountFiber(fallbackChild, nextFiber);
|
||||
}
|
||||
|
||||
if (shouldEnqueueUpdate) {
|
||||
enqueueUpdateIfNecessary(nextFiber, false);
|
||||
}
|
||||
enqueueUpdateIfNecessary(nextFiber, false);
|
||||
} else {
|
||||
let hasChildOrderChanged = false;
|
||||
if (nextFiber.child !== prevFiber.child) {
|
||||
@@ -597,7 +678,11 @@ export function attach(
|
||||
// We don't track deletions here because they are reported separately.
|
||||
if (nextChild.alternate) {
|
||||
const prevChild = nextChild.alternate;
|
||||
updateFiber(nextChild, prevChild);
|
||||
updateFiber(
|
||||
nextChild,
|
||||
prevChild,
|
||||
shouldEnqueueUpdate ? nextFiber : parentFiber
|
||||
);
|
||||
// However we also keep track if the order of the children matches
|
||||
// the previous order. They are always different referentially, but
|
||||
// if the instances line up conceptually we'll want to know that.
|
||||
@@ -605,7 +690,10 @@ export function attach(
|
||||
hasChildOrderChanged = true;
|
||||
}
|
||||
} else {
|
||||
mountFiber(nextChild);
|
||||
mountFiber(
|
||||
nextChild,
|
||||
shouldEnqueueUpdate ? nextFiber : parentFiber
|
||||
);
|
||||
if (!hasChildOrderChanged) {
|
||||
hasChildOrderChanged = true;
|
||||
}
|
||||
@@ -623,7 +711,10 @@ export function attach(
|
||||
hasChildOrderChanged = true;
|
||||
}
|
||||
}
|
||||
enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
|
||||
|
||||
if (shouldEnqueueUpdate) {
|
||||
enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,12 +723,11 @@ export function attach(
|
||||
}
|
||||
|
||||
function walkTree() {
|
||||
// Hydrate all the roots for the first time.
|
||||
hook.getFiberRoots(rendererID).forEach(root => {
|
||||
// Hydrate all the roots for the first time.
|
||||
mountFiber(root.current);
|
||||
markRootCommitted(root.current);
|
||||
mountFiber(root.current, null);
|
||||
flushPendingEvents(root);
|
||||
});
|
||||
flushPendingEvents();
|
||||
}
|
||||
|
||||
function handleCommitFiberUnmount(fiber) {
|
||||
@@ -660,21 +750,20 @@ export function attach(
|
||||
current.memoizedState != null && current.memoizedState.element != null;
|
||||
if (!wasMounted && isMounted) {
|
||||
// Mount a new root.
|
||||
mountFiber(current);
|
||||
mountFiber(current, null);
|
||||
} else if (wasMounted && isMounted) {
|
||||
// Update an existing root.
|
||||
updateFiber(current, alternate);
|
||||
updateFiber(current, alternate, null);
|
||||
} else if (wasMounted && !isMounted) {
|
||||
// Unmount an existing root.
|
||||
enqueueUnmount(current);
|
||||
}
|
||||
} else {
|
||||
// Mount a new root.
|
||||
mountFiber(current);
|
||||
mountFiber(current, null);
|
||||
}
|
||||
markRootCommitted(current);
|
||||
// We're done here.
|
||||
flushPendingEvents();
|
||||
flushPendingEvents(root);
|
||||
}
|
||||
|
||||
// The naming is confusing.
|
||||
|
||||
@@ -12,10 +12,9 @@ export type Fiber = Object;
|
||||
// TODO: If it's useful for the frontend to know which types of data an Element has
|
||||
// (e.g. props, state, context, hooks) then we could add a bitmask field for this
|
||||
// to keep the number of attributes small.
|
||||
export type RendererData = {|
|
||||
export type FiberData = {|
|
||||
key: React$Key | null,
|
||||
displayName: string | null,
|
||||
children: Array<Fiber>,
|
||||
type: ElementType,
|
||||
|};
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
export const TREE_OPERATION_ADD = 1;
|
||||
export const TREE_OPERATION_REMOVE = 2;
|
||||
export const TREE_OPERATION_RESET_CHILDREN = 3;
|
||||
+198
-204
@@ -1,73 +1,83 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
TREE_OPERATION_REMOVE,
|
||||
TREE_OPERATION_RESET_CHILDREN,
|
||||
} from '../constants';
|
||||
import { utfDecodeString } from '../utils';
|
||||
|
||||
import type { Element, ElementTreeMetadata } from './types';
|
||||
import type { Element, ElementType } from './types';
|
||||
import type { Bridge } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
console.log(`%cStore %c${methodName}`, 'color: red; font-weight: bold;', 'font-weight: bold;', ...args);
|
||||
console.log(
|
||||
`%cStore %c${methodName}`,
|
||||
'color: green; font-weight: bold;',
|
||||
'font-weight: bold;',
|
||||
...args
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* The store is the single source of truth for updates from the backend.
|
||||
* ContextProviders can subscribe to the Store for specific things they want to provide.
|
||||
*/
|
||||
class Store extends EventEmitter {
|
||||
_elementToElementTreeMetadata: WeakMap<
|
||||
Element,
|
||||
ElementTreeMetadata
|
||||
> = new WeakMap();
|
||||
_idToElement: Map<string, Element> = new Map();
|
||||
_idToParentID: Map<string, string> = new Map();
|
||||
_pendingDeletions: Set<string> = new Set();
|
||||
export default class Store extends EventEmitter {
|
||||
// TODO Should items in this map be read-only for easier props comparison?
|
||||
_idToElement: Map<number, Element> = new Map();
|
||||
|
||||
// Total number of visible elements (within all roots).
|
||||
// Used for windowing purposes.
|
||||
numElements: number = 0;
|
||||
_numElements: number = 0;
|
||||
|
||||
// This Array must be treated as immutable!
|
||||
// Passive effects will check it for changes between render and mount.
|
||||
roots: $ReadOnlyArray<string> = [];
|
||||
_roots: $ReadOnlyArray<number> = [];
|
||||
|
||||
constructor(bridge: Bridge) {
|
||||
super();
|
||||
|
||||
bridge.on('root', this.onBridgeRoot);
|
||||
bridge.on('operations', this.onBridgeOperations);
|
||||
bridge.on('rootCommitted', this.onBridgeRootCommitted);
|
||||
|
||||
bridge.on('mount', this.onBridgeMount);
|
||||
bridge.on('update', this.onBridgeUpdated);
|
||||
bridge.on('unmount', this.onBridgeUnmounted);
|
||||
}
|
||||
|
||||
getElementAtIndex(index: number): Element | null {
|
||||
get numElements(): number {
|
||||
return this._numElements;
|
||||
}
|
||||
|
||||
get roots(): $ReadOnlyArray<number> {
|
||||
return this._roots;
|
||||
}
|
||||
|
||||
getElementAtIndex(index: number): Element {
|
||||
if (index < 0 || index >= this.numElements) {
|
||||
return null;
|
||||
throw Error(`Invalid index ${index} specified`);
|
||||
}
|
||||
|
||||
// Find wich root this element is in...
|
||||
let rootID;
|
||||
let root;
|
||||
|
||||
let rootWeight = 0;
|
||||
for (let i = 0; i < this.roots.length; i++) {
|
||||
rootID = this.roots[i];
|
||||
root = this._idToElement.get(rootID);
|
||||
const { weight } = this._elementToElementTreeMetadata.get(root);
|
||||
|
||||
if (rootWeight + weight > index) {
|
||||
for (let i = 0; i < this._roots.length; i++) {
|
||||
rootID = this._roots[i];
|
||||
root = ((this._idToElement.get(rootID): any): Element);
|
||||
if (rootWeight + root.weight > index) {
|
||||
break;
|
||||
} else {
|
||||
rootWeight += root.weight;
|
||||
}
|
||||
}
|
||||
|
||||
let currentElement = root;
|
||||
// Crawl the tree to find the correct root...
|
||||
let currentElement = ((root: any): Element);
|
||||
let currentWeight = 0;
|
||||
|
||||
while (index !== currentWeight) {
|
||||
for (let i = 0; i < currentElement.children.length; i++) {
|
||||
const childID = currentElement.children[i];
|
||||
const child = this._idToElement.get(childID);
|
||||
const { weight } = this._elementToElementTreeMetadata.get(child);
|
||||
const child = ((this._idToElement.get(childID): any): Element);
|
||||
const { weight } = child;
|
||||
if (index <= currentWeight + weight) {
|
||||
currentWeight++;
|
||||
currentElement = child;
|
||||
@@ -78,207 +88,191 @@ class Store extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
return currentElement;
|
||||
return ((currentElement: any): Element);
|
||||
}
|
||||
|
||||
getElementByID(id: string) {
|
||||
return this._idToElement.get(id);
|
||||
}
|
||||
|
||||
geParent(id: string) {
|
||||
return this._idToParentID.get(id);
|
||||
}
|
||||
|
||||
getTreeMetadataForElement(element: Element) {
|
||||
return this._elementToElementTreeMetadata.get(element);
|
||||
}
|
||||
|
||||
_crawlForTreeMetadata(id: string, depth: number = 0): number {
|
||||
let weight = 1;
|
||||
|
||||
getElementByID(id: number): Element {
|
||||
const element = this._idToElement.get(id);
|
||||
|
||||
// TODO: Figure out why sometimes items aren't being sent across the bridge.
|
||||
// It always seems to be one of the ListItems...
|
||||
if (element == null) {
|
||||
console.log(`%cNo element found for id "${id}"`, 'background-color: yellow; font-weight: bold;');
|
||||
return 0;
|
||||
throw Error(`No element found with id "${id}`);
|
||||
}
|
||||
|
||||
element.children.forEach(childID => {
|
||||
weight += this._crawlForTreeMetadata(childID, depth + 1);
|
||||
});
|
||||
|
||||
this._elementToElementTreeMetadata.set(element, {
|
||||
depth,
|
||||
weight,
|
||||
});
|
||||
|
||||
return weight;
|
||||
return ((element: any): Element);
|
||||
}
|
||||
|
||||
_updateElementTreeMetadata(prevElement: Element, element: Element): void {
|
||||
if (prevElement.children === element.children) {
|
||||
return;
|
||||
}
|
||||
onBridgeOperations = (operations: Uint32Array) => {
|
||||
debug('onBridgeOperations', operations);
|
||||
|
||||
// Compare children in case they have changed.
|
||||
// For each child that was removed, we need to shrink the list by this many elements.
|
||||
// For each child that was added, we need to grow the list by this many elements.
|
||||
let haveRootsChanged = false;
|
||||
|
||||
const prevChildren = prevElement.children;
|
||||
const prevNumChildren = prevChildren.length;
|
||||
let i = 0;
|
||||
while (i < operations.length) {
|
||||
let id: number = ((null: any): number);
|
||||
let element: Element = ((null: any): Element);
|
||||
let parentID: number = ((null: any): number);
|
||||
let parentElement: Element = ((null: any): Element);
|
||||
let type: ElementType = ((null: any): ElementType);
|
||||
let weightDelta: number = 0;
|
||||
|
||||
const children = element.children;
|
||||
const numChildren = children.length;
|
||||
const operation = operations[i];
|
||||
|
||||
// TODO: The below diffing could be optimized more.
|
||||
switch (operation) {
|
||||
case TREE_OPERATION_ADD:
|
||||
id = ((operations[i + 1]: any): number);
|
||||
type = ((operations[i + 2]: any): ElementType);
|
||||
parentID = ((operations[i + 3]: any): number);
|
||||
|
||||
// Scan for deletions
|
||||
for (let i = 0; i < prevNumChildren; i++) {
|
||||
const childID = prevChildren[i];
|
||||
if (!children.includes(childID)) {
|
||||
const child = this._idToElement.get(childID);
|
||||
const { weight } = this._elementToElementTreeMetadata.get(child);
|
||||
i = i + 4;
|
||||
|
||||
this.numElements -= weight;
|
||||
if (parentID === 0) {
|
||||
debug('Add', `new root fiber ${id}`);
|
||||
|
||||
let current = element;
|
||||
while (current !== null) {
|
||||
const datum = this._elementToElementTreeMetadata.get(current);
|
||||
datum.weight -= weight;
|
||||
this._roots = this._roots.concat(id);
|
||||
|
||||
const parent = this._idToElement.get(datum.parentID);
|
||||
current =
|
||||
parent != null
|
||||
? this._elementToElementTreeMetadata.get(parent)
|
||||
: null;
|
||||
}
|
||||
this._idToElement.set(id, {
|
||||
children: [],
|
||||
depth: 0,
|
||||
displayName: null,
|
||||
id,
|
||||
key: null,
|
||||
parentID: 0,
|
||||
type,
|
||||
weight: 1,
|
||||
});
|
||||
|
||||
haveRootsChanged = true;
|
||||
} else {
|
||||
const displayNameLength = operations[i];
|
||||
i++;
|
||||
const displayName =
|
||||
displayNameLength === 0
|
||||
? null
|
||||
: utfDecodeString(
|
||||
(operations.slice(i, i + displayNameLength): any)
|
||||
);
|
||||
i += displayNameLength;
|
||||
|
||||
const keyLength = operations[i];
|
||||
i++;
|
||||
const key =
|
||||
keyLength === 0
|
||||
? null
|
||||
: utfDecodeString((operations.slice(i, i + keyLength): any));
|
||||
i += +keyLength;
|
||||
|
||||
debug('Add', `fiber ${id}, type ${type}, as child of ${parentID}`);
|
||||
|
||||
// TODO Fix this; there should not be duplicate "ADD" operations for a given element.
|
||||
if (this._idToElement.has(id)) {
|
||||
console.warn(
|
||||
`fiber ${id}, type ${type}, already added as child of ${parentID}`
|
||||
);
|
||||
} else {
|
||||
parentElement = ((this._idToElement.get(parentID): any): Element);
|
||||
parentElement.children = parentElement.children.concat(id);
|
||||
|
||||
this._idToElement.set(id, {
|
||||
children: [],
|
||||
depth: parentElement.depth + 1,
|
||||
displayName,
|
||||
id,
|
||||
key,
|
||||
parentID: parentElement.id,
|
||||
type,
|
||||
weight: 1,
|
||||
});
|
||||
|
||||
weightDelta = 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TREE_OPERATION_REMOVE:
|
||||
id = ((operations[i + 1]: any): number);
|
||||
|
||||
i = i + 2;
|
||||
|
||||
debug('Remove', `fiber ${id} from tree`);
|
||||
|
||||
element = ((this._idToElement.get(id): any): Element);
|
||||
parentID = element.parentID;
|
||||
|
||||
weightDelta = -element.weight;
|
||||
|
||||
this._idToElement.delete(id);
|
||||
|
||||
parentElement = ((this._idToElement.get(parentID): any): Element);
|
||||
if (parentElement != null) {
|
||||
parentElement.children = parentElement.children.filter(
|
||||
childID => childID !== id
|
||||
);
|
||||
}
|
||||
break;
|
||||
case TREE_OPERATION_RESET_CHILDREN:
|
||||
id = ((operations[i + 1]: any): number);
|
||||
const numChildren = ((operations[i + 2]: any): number);
|
||||
const children = ((operations.slice(
|
||||
i + 3,
|
||||
i + 3 + numChildren
|
||||
): any): Array<number>);
|
||||
|
||||
i = i + 3 + numChildren;
|
||||
|
||||
debug('Re-order', `fiber ${id} children ${children.join(',')}`);
|
||||
|
||||
element = ((this._idToElement.get(id): any): Element);
|
||||
element.children = children;
|
||||
|
||||
const prevWeight = element.weight;
|
||||
let childWeight = 0;
|
||||
|
||||
children.forEach(childID => {
|
||||
const child = ((this._idToElement.get(childID): any): Element);
|
||||
childWeight += child.weight;
|
||||
});
|
||||
|
||||
element.weight = childWeight + 1;
|
||||
weightDelta = childWeight + 1 - prevWeight;
|
||||
break;
|
||||
default:
|
||||
throw Error(`Unsupported Bridge operation ${operation}`);
|
||||
}
|
||||
|
||||
this._numElements += weightDelta;
|
||||
|
||||
while (parentElement != null) {
|
||||
parentElement.weight += weightDelta;
|
||||
parentElement = ((this._idToElement.get(
|
||||
parentElement.parentID
|
||||
): any): Element);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for additions
|
||||
for (let i = 0; i < numChildren; i++) {
|
||||
const childID = children[i];
|
||||
if (!prevChildren.includes(childID)) {
|
||||
const child = this._idToElement.get(childID);
|
||||
const { depth } = this._elementToElementTreeMetadata.get(element);
|
||||
const weight = this._crawlForTreeMetadata(
|
||||
childID,
|
||||
depth + 1
|
||||
);
|
||||
|
||||
this.numElements += weight;
|
||||
|
||||
let current = element;
|
||||
while (current !== null) {
|
||||
const datum = this._elementToElementTreeMetadata.get(current);
|
||||
datum.weight += weight;
|
||||
|
||||
const parent = this._idToElement.get(datum.parentID);
|
||||
current =
|
||||
parent != null
|
||||
? this._elementToElementTreeMetadata.get(parent)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
__printTree() {
|
||||
let i = 0;
|
||||
this.roots.forEach(rootID => {
|
||||
const root = this._idToElement.get(rootID);
|
||||
const { weight } = this._elementToElementTreeMetadata.get(root);
|
||||
for (let j = i; j < i + weight; j++) {
|
||||
const element = this.getElementAtIndex(j)
|
||||
const { depth } = this._elementToElementTreeMetadata.get(element);
|
||||
|
||||
console.log(' '.repeat(depth) + element.displayName);
|
||||
}
|
||||
i += weight;
|
||||
});
|
||||
}
|
||||
|
||||
onBridgeMount = (element: Element) => {
|
||||
const { id } = element;
|
||||
debug('onBridgeMount()', element);
|
||||
this._idToElement.set(id, element);
|
||||
|
||||
element.children.forEach(childID => {
|
||||
this._idToParentID.set(childID, id);
|
||||
});
|
||||
|
||||
this.emit(id);
|
||||
};
|
||||
|
||||
onBridgeRoot = (id: string) => {
|
||||
debug('onBridgeRoot()', id);
|
||||
if (!this.roots.includes(id)) {
|
||||
this.roots = this.roots.concat(id);
|
||||
|
||||
// Generate tree metadata used for windowing.
|
||||
this.numElements += this._crawlForTreeMetadata(id);
|
||||
|
||||
if (haveRootsChanged) {
|
||||
this.emit('roots');
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeRootCommitted = (rootID: string) => {
|
||||
this._pendingDeletions.forEach(id => {
|
||||
this._idToElement.delete(id);
|
||||
onBridgeRootCommitted = (rootID: number) => {
|
||||
debug('onBridgeRootCommitted', rootID);
|
||||
|
||||
if (this._idToParentID.has(id)) {
|
||||
this._idToParentID.delete(id);
|
||||
}
|
||||
});
|
||||
this._pendingDeletions.clear();
|
||||
this.emit('rootCommitted');
|
||||
|
||||
debug('onBridgeRootCommitted()', rootID);
|
||||
this.emit('rootCommitted', rootID);
|
||||
|
||||
this.__printTree();
|
||||
// this.__printTree(rootID);
|
||||
};
|
||||
|
||||
// TODO: Unmounting removes id-to-element before crawling, which breaks it.
|
||||
// Should I just ditch the idea of a WeakMap in favor of an explicit it-to-metadata mapping like with parents?
|
||||
onBridgeUnmounted = (id: string) => {
|
||||
debug('onBridgeUnmounted()', id);
|
||||
this._pendingDeletions.add(id);
|
||||
|
||||
const index = this.roots.indexOf(id);
|
||||
if (index >= 0) {
|
||||
this.roots = this.roots
|
||||
.slice(0, index)
|
||||
.concat(this.roots.slice(index + 1));
|
||||
|
||||
const root = this._idToElement.get(id);
|
||||
const {weight} = this._elementToElementTreeMetadata.get(root);
|
||||
|
||||
this.numElements -= weight;
|
||||
|
||||
this.emit('roots');
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeUpdated = (element: Element) => {
|
||||
const { id } = element;
|
||||
debug('onBridgeUpdated()', element);
|
||||
|
||||
const prevElement = ((this._idToElement.get(id): any): Element);
|
||||
const prevElementTreeMetadata = ((this._elementToElementTreeMetadata.get(prevElement): any): ElementTreeMetadata);
|
||||
|
||||
this._idToElement.set(id, element);
|
||||
this._elementToElementTreeMetadata.set(
|
||||
element,
|
||||
prevElementTreeMetadata
|
||||
);
|
||||
|
||||
// Update tree metadata used for windowing.
|
||||
this._updateElementTreeMetadata(prevElement, element);
|
||||
|
||||
this.emit(id);
|
||||
// DEBUG
|
||||
__printTree = (rootID: number) => {
|
||||
const printElement = (id: number) => {
|
||||
const element = ((this._idToElement.get(id): any): Element);
|
||||
console.log(
|
||||
`${' '.repeat(element.depth)}${element.id}:${element.displayName ||
|
||||
''}${element.key ? `key:"${element.key}"` : ''} (${element.weight})`
|
||||
);
|
||||
element.children.forEach(printElement);
|
||||
};
|
||||
const root = ((this._idToElement.get(rootID): any): Element);
|
||||
console.log('printing root:', rootID);
|
||||
root.children.forEach(printElement);
|
||||
};
|
||||
}
|
||||
|
||||
export default Store;
|
||||
|
||||
+13
-15
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import typeof Store from './Store';
|
||||
import Store from './Store';
|
||||
|
||||
export const ElementTypeClassOrFunction = 1;
|
||||
export const ElementTypeContext = 2;
|
||||
@@ -8,28 +8,26 @@ export const ElementTypeForwardRef = 3;
|
||||
export const ElementTypeMemo = 4;
|
||||
export const ElementTypeOtherOrUnknown = 5;
|
||||
export const ElementTypeProfiler = 6;
|
||||
export const ElementTypeSuspense = 7;
|
||||
export const ElementTypeRoot = 7;
|
||||
export const ElementTypeSuspense = 8;
|
||||
|
||||
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
|
||||
// TODO: Add profiling node
|
||||
|
||||
export type Element = {|
|
||||
id: string,
|
||||
type: ElementType,
|
||||
key: React$Key | null,
|
||||
displayName: string | null,
|
||||
children: Array<string>,
|
||||
|};
|
||||
|
||||
export type ElementTreeMetadata = {|
|
||||
children: Array<number>,
|
||||
depth: number,
|
||||
rootID: string,
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
key: number | string | null,
|
||||
parentID: number,
|
||||
type: ElementType,
|
||||
weight: number,
|
||||
|};
|
||||
|
||||
export type InspectedElement = {|
|
||||
id: string,
|
||||
id: number,
|
||||
context: Object | null,
|
||||
hooks: Object | null,
|
||||
props: Object | null,
|
||||
@@ -38,7 +36,7 @@ export type InspectedElement = {|
|
||||
source: Object,
|
||||
|};
|
||||
|
||||
export type TreeContext = {|
|
||||
export type TreeMetadataType = {|
|
||||
size: number,
|
||||
store: Store,
|
||||
|};
|
||||
|};
|
||||
|
||||
@@ -5,13 +5,13 @@ import { TreeContext } from './contexts';
|
||||
|
||||
import styles from './Element.css';
|
||||
|
||||
type Props = {|
|
||||
type Props = {
|
||||
index: number,
|
||||
style: Object,
|
||||
|};
|
||||
};
|
||||
|
||||
export default function Element({ index, style }: Props) {
|
||||
const {store} = useContext(TreeContext);
|
||||
const { store } = useContext(TreeContext);
|
||||
const element = store.getElementAtIndex(index);
|
||||
|
||||
// DevTools are rendered in concurrent mode.
|
||||
@@ -22,10 +22,7 @@ export default function Element({ index, style }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const elementTreeMetadata = store.getTreeMetadataForElement(element);
|
||||
|
||||
const { children, displayName, key } = element;
|
||||
const { depth } = elementTreeMetadata;
|
||||
const { children, depth, displayName, key } = element;
|
||||
|
||||
// TODO: Add state for toggling element open/close
|
||||
|
||||
@@ -34,7 +31,7 @@ export default function Element({ index, style }: Props) {
|
||||
className={styles.Element}
|
||||
style={{
|
||||
...style,
|
||||
paddingLeft: `${1 + depth}rem`
|
||||
paddingLeft: `${1 + depth}rem`,
|
||||
}}
|
||||
>
|
||||
{children.length > 0 && <span className={styles.ArrowOpen} />}
|
||||
|
||||
@@ -15,12 +15,12 @@ export type Props = {|
|
||||
|};
|
||||
|
||||
export default function Elements({ bridge, browserName, themeName }: Props) {
|
||||
const store = useMemo(() => new Store(bridge), []);
|
||||
const store = useMemo<Store>(() => new Store(bridge), []);
|
||||
|
||||
const [treeContext, setTreeContext] = useState(({
|
||||
const [treeContext, setTreeContext] = useState({
|
||||
size: store.numElements,
|
||||
store,
|
||||
}));
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handler = () => {
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { TreeMetadata } from '../types';
|
||||
import type { TreeMetadataType } from '../types';
|
||||
|
||||
import Store from '../store';
|
||||
|
||||
export const RootsContext = createContext<Array<string>>([]);
|
||||
export const StoreContext = createContext<Store>(((null: any): Store));
|
||||
export const TreeContext = createContext<TreeMetadata>(((null: any): TreeMetadata));
|
||||
export const TreeContext = createContext<TreeMetadataType>(
|
||||
((null: any): TreeMetadataType)
|
||||
);
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
import type { Element, ElementTreeMetadata } from '../types';
|
||||
import type { Element } from '../types';
|
||||
import Store from '../store';
|
||||
|
||||
// TODO useEffect has a bug where sometimes subscriptions don't get cleaned up correctly.
|
||||
// Potentially related to github.com/facebookincubator/redux-react-hook/issues/17
|
||||
// As a temporary work around, switch back to layout effect.
|
||||
|
||||
export function useElement(store: Store, id: string): ?Element {
|
||||
export function useElement(store: Store, id: number): ?Element {
|
||||
const [element, setElement] = useState<?Element>(store.getElementByID(id));
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@@ -17,7 +17,7 @@ export function useElement(store: Store, id: string): ?Element {
|
||||
setElement(((store.getElementByID(id): any): Element));
|
||||
|
||||
// Listen for changes to the element.
|
||||
store.addListener(id, handler);
|
||||
store.addListener((id: any), handler);
|
||||
|
||||
// Check for changes that may have happened between render and mount.
|
||||
const newElement = store.getElementByID(id);
|
||||
@@ -26,14 +26,14 @@ export function useElement(store: Store, id: string): ?Element {
|
||||
}
|
||||
|
||||
// Remove event listener on unmount.
|
||||
return () => store.removeListener(id, handler);
|
||||
return () => store.removeListener((id: any), handler);
|
||||
}, [store, id]);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export function useRoots(store: Store): $ReadOnlyArray<string> {
|
||||
const [roots, setRoots] = useState<$ReadOnlyArray<string>>(store.roots);
|
||||
export function useRoots(store: Store): $ReadOnlyArray<number> {
|
||||
const [roots, setRoots] = useState<$ReadOnlyArray<number>>(store.roots);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const handler = () => setRoots(store.roots);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
|
||||
export function utfDecodeString(array: Uint8Array): string {
|
||||
let string = '';
|
||||
const { length } = array;
|
||||
for (let i = 0; i < length; i++) {
|
||||
string += String.fromCharCode(array[i]);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
export function utfEncodeString(string: string): Uint8Array {
|
||||
const array = new Uint8Array(string.length);
|
||||
const { length } = string;
|
||||
for (let i = 0; i < length; i++) {
|
||||
array[i] = string.charCodeAt(i);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
@@ -8422,10 +8422,10 @@ react-virtualized-auto-sizer@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
||||
integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
|
||||
|
||||
react-window@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.5.0.tgz#3e56b720b97666bce38a9e932bdd238d56e258f1"
|
||||
integrity sha512-55WeZKjMNF5JdCuKghc/H65DBecoeGgH8MOX3CgT7BJ66xb4ITRuXPUlz0qU6r50wetdF/oLhorYBRvKD4Z1IQ==
|
||||
react-window@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.5.1.tgz#9d68624d5ba58ddf331321fb74b0a75184523d16"
|
||||
integrity sha512-D855yW104ek8+gSLW8G6Dt69WnhSlGXR4Ld+IoZW4aQg6a1pt7cZLb+fcOugVDVgl4RfrLSfg9aLBnvS7zsOaw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one "^3.1.1"
|
||||
|
||||
Reference in New Issue
Block a user