Files
react/src/backend/agent.js
T

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;
}