mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Refactored insepected element cache to use the context API
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { createResource } from './cache';
|
||||
import Store from './store';
|
||||
import { hydrate } from 'src/hydration';
|
||||
|
||||
import type {
|
||||
DehydratedData,
|
||||
InspectedElement,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Resource } from './cache';
|
||||
import type { Bridge } from '../types';
|
||||
|
||||
type ResolveFn = (inspectedElement: InspectedElement) => void;
|
||||
|
||||
type Params = {|
|
||||
id: number,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
// TODO Use an LRU for the underlying caching mechanism, to prevent memory leaks.
|
||||
|
||||
// TODO Something needs to poll for (unprompted) updates.
|
||||
|
||||
export default class InspectedElementCache extends EventEmitter {
|
||||
_bridge: Bridge;
|
||||
_store: Store;
|
||||
|
||||
_pendingRequests: Map<number, ResolveFn> = new Map();
|
||||
|
||||
_resource: Resource<Params, number, InspectedElement> = createResource(
|
||||
({ id, rendererID }: Params) => {
|
||||
return new Promise(resolve => {
|
||||
this._pendingRequests.set(id, resolve);
|
||||
this._bridge.send('inspectElement', {
|
||||
id,
|
||||
rendererID,
|
||||
});
|
||||
});
|
||||
},
|
||||
({ id, rendererID }: Params) => id
|
||||
);
|
||||
|
||||
constructor(bridge: Bridge, store: Store) {
|
||||
super();
|
||||
|
||||
this._bridge = bridge;
|
||||
this._store = store;
|
||||
|
||||
bridge.addListener('inspectedElement', this._onInspectedElement);
|
||||
}
|
||||
|
||||
read(id: number): InspectedElement | null {
|
||||
const rendererID = this._store.getRendererIDForElement(id);
|
||||
|
||||
if (rendererID != null) {
|
||||
return this._resource.read({ id, rendererID });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_onInspectedElement = (inspectedElement: InspectedElement) => {
|
||||
const id = inspectedElement.id;
|
||||
|
||||
if (inspectedElement != null) {
|
||||
inspectedElement.context = hydrateHelper(inspectedElement.context);
|
||||
inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
|
||||
inspectedElement.props = hydrateHelper(inspectedElement.props);
|
||||
inspectedElement.state = hydrateHelper(inspectedElement.state);
|
||||
}
|
||||
|
||||
const resolveFn = this._pendingRequests.get(id);
|
||||
if (resolveFn != null) {
|
||||
this._pendingRequests.delete(id);
|
||||
|
||||
resolveFn(inspectedElement);
|
||||
} else {
|
||||
this._resource.write(id, inspectedElement);
|
||||
|
||||
this.emit('invalidated', id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+13
-21
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { createContext } from 'react';
|
||||
import LRU from 'lru-cache';
|
||||
|
||||
// Cache implementation was forked from the React repo:
|
||||
// https://github.com/facebook/react/blob/master/packages/react-cache/src/ReactCache.js
|
||||
@@ -63,10 +64,6 @@ function readContext(Context, observedBits) {
|
||||
return dispatcher.readContext(Context, observedBits);
|
||||
}
|
||||
|
||||
function identityHashFn(input) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const CacheContext = createContext(null);
|
||||
|
||||
const entries: Map<Resource<any, any, any>, Map<any, any>> = new Map();
|
||||
@@ -77,12 +74,8 @@ function accessResult<Input, Key, Value>(
|
||||
input: Input,
|
||||
key: Key
|
||||
): Result<Value> {
|
||||
let entriesForResource = entries.get(resource);
|
||||
if (entriesForResource === undefined) {
|
||||
entriesForResource = new Map();
|
||||
entries.set(resource, entriesForResource);
|
||||
}
|
||||
let entry = entriesForResource.get(key);
|
||||
const entriesForResource = ((entries.get(resource): any): Map<any, any>);
|
||||
const entry = entriesForResource.get(key);
|
||||
if (entry === undefined) {
|
||||
const thenable = fetch(input);
|
||||
thenable.then(
|
||||
@@ -114,16 +107,16 @@ function accessResult<Input, Key, Value>(
|
||||
|
||||
export function createResource<Input, Key: string | number, Value>(
|
||||
fetch: Input => Thenable<Value>,
|
||||
maybeHashInput?: Input => Key
|
||||
hashInput: Input => Key,
|
||||
useLRU?: boolean = false
|
||||
): Resource<Input, Key, Value> {
|
||||
const hashInput: Input => Key =
|
||||
maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
|
||||
|
||||
const resource = {
|
||||
invalidate(key: Key): void {
|
||||
const entriesForResource = entries.get(resource);
|
||||
if (entriesForResource !== undefined) {
|
||||
const entriesForResource = ((entries.get(resource): any): Map<any, any>);
|
||||
if (entriesForResource instanceof Map) {
|
||||
entriesForResource.delete(key);
|
||||
} else {
|
||||
entriesForResource.set(key, undefined);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -163,14 +156,13 @@ export function createResource<Input, Key: string | number, Value>(
|
||||
},
|
||||
|
||||
write(key: Key, value: Value): void {
|
||||
let entriesForResource = entries.get(resource);
|
||||
if (entriesForResource === undefined) {
|
||||
entriesForResource = new Map();
|
||||
entries.set(resource, entriesForResource);
|
||||
}
|
||||
const entriesForResource = ((entries.get(resource): any): Map<any, any>);
|
||||
entriesForResource.set(key, value);
|
||||
},
|
||||
};
|
||||
|
||||
entries.set(resource, useLRU ? new LRU({ max: 10 }) : new Map());
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ElementTypeRoot } from './types';
|
||||
import { utfDecodeString } from '../utils';
|
||||
import { __DEBUG__ } from '../constants';
|
||||
import ProfilingCache from './ProfilingCache';
|
||||
import InspectedElementCache from './InspectedElementCache';
|
||||
|
||||
import type { ElementType } from './types';
|
||||
import type { Element } from './views/Components/types';
|
||||
@@ -76,9 +75,6 @@ export default class Store extends EventEmitter {
|
||||
// The user has imported a previously exported profiling session.
|
||||
_importedProfilingData: ImportedProfilingData | null = null;
|
||||
|
||||
// Suspense cache for lazy-loaded inspected Element data.
|
||||
_inspectedElementCache: InspectedElementCache;
|
||||
|
||||
// The backend is currently profiling.
|
||||
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
|
||||
_isProfiling: boolean = false;
|
||||
@@ -174,7 +170,6 @@ export default class Store extends EventEmitter {
|
||||
// so the frontend needs to ask the backend for its status after mounting.
|
||||
bridge.send('getProfilingStatus');
|
||||
|
||||
this._inspectedElementCache = new InspectedElementCache(bridge, this);
|
||||
this._profilingCache = new ProfilingCache(bridge, this);
|
||||
}
|
||||
|
||||
@@ -229,10 +224,6 @@ export default class Store extends EventEmitter {
|
||||
this.emit('importedProfilingData');
|
||||
}
|
||||
|
||||
get inspectedElementCache(): InspectedElementCache {
|
||||
return this._inspectedElementCache;
|
||||
}
|
||||
|
||||
get isProfiling(): boolean {
|
||||
return this._isProfiling;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import Tree from './Tree';
|
||||
import SelectedElement from './SelectedElement';
|
||||
import styles from './Components.css';
|
||||
import { InspectedElementContextController } from './InspectedElementContext';
|
||||
import portaledContent from '../portaledContent';
|
||||
|
||||
import styles from './Components.css';
|
||||
|
||||
function Components(_: {||}) {
|
||||
// TODO Flex wrappers below should be user resizable.
|
||||
return (
|
||||
@@ -14,9 +16,11 @@ function Components(_: {||}) {
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
<InspectedElementContextController>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</InspectedElementContextController>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createResource } from '../../cache';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import { hydrate } from 'src/hydration';
|
||||
|
||||
import type {
|
||||
DehydratedData,
|
||||
InspectedElement,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Resource } from '../../cache';
|
||||
|
||||
// TODO Something needs to poll for (unprompted) updates.
|
||||
|
||||
// TODO The curretn approach caches resources permanently.
|
||||
// We won't even ask for an update if an element is reselected.
|
||||
// I think we need to separate the polling for an update from the suspense cache.
|
||||
// This way we can always resened (and poll on an interval) for the selected id,
|
||||
// and the cache here can just invalidate itself as responses stream in.
|
||||
|
||||
type Params = {|
|
||||
id: number,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type Context = {|
|
||||
read(id: number): InspectedElement | null,
|
||||
|};
|
||||
|
||||
const InspectedElementContext = createContext<Context>(((null: any): Context));
|
||||
InspectedElementContext.displayName = 'InspectedElementContext';
|
||||
|
||||
type ResolveFn = (inspectedElement: InspectedElement) => void;
|
||||
type InProgressRequest = {|
|
||||
promise: Promise<InspectedElement>,
|
||||
resolveFn: ResolveFn,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
function InspectedElementContextController({ children }: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
const inProgressRequests = useMemo<Map<number, InProgressRequest>>(
|
||||
() => new Map(),
|
||||
[]
|
||||
);
|
||||
|
||||
const resource = useMemo<Resource<Params, number, InspectedElement>>(
|
||||
() =>
|
||||
createResource(
|
||||
({ id, rendererID }: Params) => {
|
||||
let request = inProgressRequests.get(id);
|
||||
if (request != null) {
|
||||
return request.promise;
|
||||
}
|
||||
|
||||
let resolveFn = ((null: any): ResolveFn);
|
||||
const promise = new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
|
||||
bridge.send('inspectElement', { id, rendererID });
|
||||
});
|
||||
|
||||
inProgressRequests.set(id, { promise, resolveFn });
|
||||
|
||||
return promise;
|
||||
},
|
||||
({ id, rendererID }: Params) => id
|
||||
),
|
||||
[bridge, inProgressRequests]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onInspectedElement = (inspectedElement: InspectedElement | null) => {
|
||||
if (inspectedElement != null) {
|
||||
const id = inspectedElement.id;
|
||||
|
||||
inspectedElement.context = hydrateHelper(inspectedElement.context);
|
||||
inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
|
||||
inspectedElement.props = hydrateHelper(inspectedElement.props);
|
||||
inspectedElement.state = hydrateHelper(inspectedElement.state);
|
||||
|
||||
const request = inProgressRequests.get(id);
|
||||
if (request != null) {
|
||||
inProgressRequests.delete(id);
|
||||
request.resolveFn(inspectedElement);
|
||||
} else {
|
||||
resource.write(id, inspectedElement);
|
||||
|
||||
// Schedule update with React.
|
||||
setCount(count => count + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bridge.addListener('inspectedElement', onInspectedElement);
|
||||
return () => bridge.removeListener('inspectElement', onInspectedElement);
|
||||
}, [bridge, inProgressRequests, resource]);
|
||||
|
||||
const read = useCallback(
|
||||
(id: number) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID != null) {
|
||||
return resource.read({ id, rendererID });
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[resource, store]
|
||||
);
|
||||
|
||||
// "count" is intentionally passed so that it recreates the memoized object.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const value = useMemo(() => ({ read }), [count, read]);
|
||||
|
||||
return (
|
||||
<InspectedElementContext.Provider value={value}>
|
||||
{children}
|
||||
</InspectedElementContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export { InspectedElementContext, InspectedElementContextController };
|
||||
@@ -7,6 +7,7 @@ import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import HooksTree from './HooksTree';
|
||||
import InspectedElementTree from './InspectedElementTree';
|
||||
import { InspectedElementContext } from './InspectedElementContext';
|
||||
import styles from './SelectedElement.css';
|
||||
import {
|
||||
ElementTypeClass,
|
||||
@@ -25,13 +26,13 @@ export default function SelectedElement(_: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
const { read } = useContext(InspectedElementContext);
|
||||
|
||||
const element =
|
||||
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
|
||||
|
||||
const inspectedElement =
|
||||
selectedElementID != null
|
||||
? store.inspectedElementCache.read(selectedElementID)
|
||||
: null;
|
||||
selectedElementID != null ? read(selectedElementID) : null;
|
||||
|
||||
const highlightElement = useCallback(() => {
|
||||
if (element !== null && selectedElementID !== null) {
|
||||
|
||||
Reference in New Issue
Block a user