mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
196 lines
5.1 KiB
JavaScript
196 lines
5.1 KiB
JavaScript
// @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 { 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);
|
|
};
|
|
|
|
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();
|
|
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
|
|
_roots: Set<RendererID> = new Set();
|
|
|
|
addBridge(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(
|
|
rendererID: RendererID,
|
|
rendererInterface: RendererInterface
|
|
) {
|
|
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);
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
onHookUpdate = ({ data, fiber }: { data: RendererData, fiber: Fiber }) => {
|
|
const id = this._getId(fiber);
|
|
|
|
this._idToRendererData.set(id, data);
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|