mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
284 lines
8.1 KiB
JavaScript
284 lines
8.1 KiB
JavaScript
// @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, ElementType } from './types';
|
|
import type { Bridge } from '../types';
|
|
|
|
const debug = (methodName, ...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.
|
|
*/
|
|
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;
|
|
|
|
// This Array must be treated as immutable!
|
|
// Passive effects will check it for changes between render and mount.
|
|
_roots: $ReadOnlyArray<number> = [];
|
|
|
|
constructor(bridge: Bridge) {
|
|
super();
|
|
|
|
bridge.on('operations', this.onBridgeOperations);
|
|
bridge.on('rootCommitted', this.onBridgeRootCommitted);
|
|
}
|
|
|
|
get numElements(): number {
|
|
return this._numElements;
|
|
}
|
|
|
|
get roots(): $ReadOnlyArray<number> {
|
|
return this._roots;
|
|
}
|
|
|
|
getElementAtIndex(index: number): Element {
|
|
if (index < 0 || index >= this.numElements) {
|
|
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): any): Element);
|
|
if (rootWeight + root.weight > index) {
|
|
break;
|
|
} else {
|
|
rootWeight += root.weight;
|
|
}
|
|
}
|
|
|
|
// 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): any): Element);
|
|
const { weight } = child;
|
|
if (index <= currentWeight + weight) {
|
|
currentWeight++;
|
|
currentElement = child;
|
|
break;
|
|
} else {
|
|
currentWeight += weight;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ((currentElement: any): Element);
|
|
}
|
|
|
|
getElementByID(id: number): Element {
|
|
const element = this._idToElement.get(id);
|
|
|
|
if (element == null) {
|
|
throw Error(`No element found with id "${id}`);
|
|
}
|
|
|
|
return ((element: any): Element);
|
|
}
|
|
|
|
onBridgeOperations = (operations: Uint32Array) => {
|
|
if (!(operations instanceof Uint32Array)) {
|
|
// $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
|
|
operations = Uint32Array.from(Object.values(operations));
|
|
}
|
|
|
|
debug('onBridgeOperations', operations);
|
|
|
|
let haveRootsChanged = false;
|
|
|
|
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 operation = operations[i];
|
|
|
|
switch (operation) {
|
|
case TREE_OPERATION_ADD:
|
|
id = ((operations[i + 1]: any): number);
|
|
type = ((operations[i + 2]: any): ElementType);
|
|
parentID = ((operations[i + 3]: any): number);
|
|
|
|
i = i + 4;
|
|
|
|
if (parentID === 0) {
|
|
debug('Add', `new root fiber ${id}`);
|
|
|
|
this._roots = this._roots.concat(id);
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (haveRootsChanged) {
|
|
this.emit('roots');
|
|
}
|
|
};
|
|
|
|
onBridgeRootCommitted = (rootID: number) => {
|
|
debug('onBridgeRootCommitted', rootID);
|
|
|
|
this.emit('rootCommitted');
|
|
|
|
// this.__printTree(rootID);
|
|
};
|
|
|
|
// 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);
|
|
};
|
|
}
|