mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works. When the frontend polls the backend for an update on the element that's currently inspected, the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked. In thid case, the frontend cache should reuse the previous (cached) value. Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store. This doens't work properly though when component filters are changed, because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects). So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import LRU from 'lru-cache';
|
|
import {
|
|
convertInspectedElementBackendToFrontend,
|
|
hydrateHelper,
|
|
inspectElement as inspectElementAPI,
|
|
} from 'react-devtools-shared/src/backendAPI';
|
|
import {fillInPath} from 'react-devtools-shared/src/hydration';
|
|
|
|
import type {LRUCache} from 'react-devtools-shared/src/types';
|
|
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
|
import type {
|
|
InspectElementFullData,
|
|
InspectElementHydratedPath,
|
|
} from 'react-devtools-shared/src/backend/types';
|
|
import type {
|
|
Element,
|
|
InspectedElement as InspectedElementFrontend,
|
|
InspectedElementResponseType,
|
|
} from 'react-devtools-shared/src/devtools/views/Components/types';
|
|
|
|
// Maps element ID to inspected data.
|
|
// We use an LRU for this rather than a WeakMap because of how the "no-change" optimization works.
|
|
// When the frontend polls the backend for an update on the element that's currently inspected,
|
|
// the backend will send a "no-change" message if the element hasn't updated (rendered) since the last time it was asked.
|
|
// In thid case, the frontend cache should reuse the previous (cached) value.
|
|
// Using a WeakMap keyed on Element generally works well for this, since Elements are mutable and stable in the Store.
|
|
// This doens't work properly though when component filters are changed,
|
|
// because this will cause the Store to dump all roots and re-initialize the tree (recreating the Element objects).
|
|
// So instead we key on Element ID (which is stable in this case) and use an LRU for eviction.
|
|
const inspectedElementCache: LRUCache<
|
|
number,
|
|
InspectedElementFrontend,
|
|
> = new LRU({
|
|
max: 25,
|
|
});
|
|
|
|
type Path = Array<string | number>;
|
|
|
|
type InspectElementReturnType = [
|
|
InspectedElementFrontend,
|
|
InspectedElementResponseType,
|
|
];
|
|
|
|
export function inspectElement({
|
|
bridge,
|
|
element,
|
|
path,
|
|
rendererID,
|
|
}: {|
|
|
bridge: FrontendBridge,
|
|
element: Element,
|
|
path: Path | null,
|
|
rendererID: number,
|
|
|}): Promise<InspectElementReturnType> {
|
|
const {id} = element;
|
|
return inspectElementAPI({
|
|
bridge,
|
|
id,
|
|
path,
|
|
rendererID,
|
|
}).then((data: any) => {
|
|
const {type} = data;
|
|
|
|
let inspectedElement;
|
|
switch (type) {
|
|
case 'no-change':
|
|
// This is a no-op for the purposes of our cache.
|
|
inspectedElement = inspectedElementCache.get(element.id);
|
|
if (inspectedElement != null) {
|
|
return [inspectedElement, type];
|
|
}
|
|
|
|
// We should only encounter this case in the event of a bug.
|
|
throw Error(`Cached data for element "${id}" not found`);
|
|
|
|
case 'not-found':
|
|
// This is effectively a no-op.
|
|
// If the Element is still in the Store, we can eagerly remove it from the Map.
|
|
inspectedElementCache.remove(element.id);
|
|
|
|
throw Error(`Element "${id}" not found`);
|
|
|
|
case 'full-data':
|
|
const fullData = ((data: any): InspectElementFullData);
|
|
|
|
// New data has come in.
|
|
// We should replace the data in our local mutable copy.
|
|
inspectedElement = convertInspectedElementBackendToFrontend(
|
|
fullData.value,
|
|
);
|
|
|
|
inspectedElementCache.set(element.id, inspectedElement);
|
|
|
|
return [inspectedElement, type];
|
|
|
|
case 'hydrated-path':
|
|
const hydratedPathData = ((data: any): InspectElementHydratedPath);
|
|
const {value} = hydratedPathData;
|
|
|
|
// A path has been hydrated.
|
|
// Merge it with the latest copy we have locally and resolve with the merged value.
|
|
inspectedElement = inspectedElementCache.get(element.id) || null;
|
|
if (inspectedElement !== null) {
|
|
// Clone element
|
|
inspectedElement = {...inspectedElement};
|
|
|
|
// Merge hydrated data
|
|
fillInPath(
|
|
inspectedElement,
|
|
value,
|
|
((path: any): Path),
|
|
hydrateHelper(value, ((path: any): Path)),
|
|
);
|
|
|
|
inspectedElementCache.set(element.id, inspectedElement);
|
|
|
|
return [inspectedElement, type];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Should never happen.
|
|
if (__DEV__) {
|
|
console.error(
|
|
`Unexpected inspected element response data: "${type}"`,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw Error(`Unable to inspect element with id "${id}"`);
|
|
});
|
|
}
|