Replaced node objects with typed array of tree operations; windowing works in small test harness
Brian Vaughn committed
Jan 28, 2019 at 15:50 UTC
3f93b029a413f82ef021d2b4b5dfcf224d6d07e7
14 files changed
+540
-580
package.json
+1
-1
@@ -90,7 +90,7 @@
90
"react-dom": "^16.8.0-alpha.1",
91
"react-portal": "^3.1.0",
92
"react-virtualized-auto-sizer": "^1.0.2",
93
- "react-window": "^1.5.0",
93
+ "react-window": "^1.5.1",
94
"semver": "^5.5.1",
95
"style-loader": "^0.23.1",
96
"web-ext": "^1.10.1",
src/backend/agent.js
+22
-163
@@ -1,44 +1,28 @@
1
// @flow
2
3
-import nullthrows from 'nullthrows';
3
import EventEmitter from 'events';
5
-import { guid } from './utils';
6
-import { ElementTypeOtherOrUnknown } from 'src/devtools/types';
4
8
-import type {
9
- Fiber,
10
- RendererData,
11
- RendererID,
12
- RendererInterface,
13
-} from './types';
5
+import type { RendererID, RendererInterface } from './types';
6
import type { Bridge } from '../types';
15
-import type { Element } from 'src/devtools/types';
7
8
const debug = (methodName, ...args) => {
18
- console.log(`%cAgent %c${methodName}`, 'color: blue; font-weight: bold;', 'font-weight: bold;', ...args);
9
+ console.log(
10
+ `%cAgent %c${methodName}`,
11
+ 'color: purple; font-weight: bold;',
12
+ 'font-weight: bold;',
13
+ ...args
14
+ );
15
};
16
21
-const THROTTLE_BY_MS = 350;
22
-
17
export default class Agent extends EventEmitter {
24
- _fiberToID: WeakMap<Fiber, string> = new WeakMap();
25
- _idToElement: Map<string, Element> = new Map();
26
- _idToFiber: Map<string, Fiber> = new Map();
27
- _idToRendererData: Map<string, RendererData> = new Map();
28
- _idToRendererID: Map<string, RendererID> = new Map();
18
+ _bridge: Bridge = ((null: any): Bridge);
19
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
30
- _roots: Set<RendererID> = new Set();
20
21
addBridge(bridge: Bridge) {
22
+ this._bridge = bridge;
23
+
24
// TODO Listen to bridge for things like selection.
25
// bridge.on('...'), this...);
35
-
36
- this.addListener('root', id => bridge.send('root', id));
37
- this.addListener('rootCommitted', id => bridge.send('rootCommitted', id));
38
- this.addListener('mount', data => bridge.send('mount', data));
39
- this.addListener('update', data => bridge.send('update', data));
40
- this.addListener('unmount', data => bridge.send('unmount', data));
41
- // TODO Add other methods for e.g. profiling.
26
}
27
28
setRendererInterface(
@@ -48,148 +32,23 @@ export default class Agent extends EventEmitter {
32
this._rendererInterfaces[rendererID] = rendererInterface;
33
}
34
51
- _getId(fiber: Fiber): string {
52
- if (typeof fiber !== 'object' || !fiber) {
53
- return fiber;
54
- }
55
- if (!this._fiberToID.has(fiber)) {
56
- this._fiberToID.set(fiber, guid());
57
- this._idToFiber.set(nullthrows(this._fiberToID.get(fiber)), fiber);
58
- }
59
- return nullthrows(this._fiberToID.get(fiber));
60
- }
61
-
62
- _crawl(parent: Element, id: string): void {
63
- const data: RendererData = ((this._idToRendererData.get(
64
- id
65
- ): any): RendererData);
66
- if (data.type === ElementTypeOtherOrUnknown) {
67
- data.children.forEach(childFiber => {
68
- if (childFiber !== null) {
69
- this._crawl(parent, this._getId(childFiber));
70
- }
71
- });
72
- } else {
73
- parent.children.push(id);
74
-
75
- this._createOrUpdateElement(id, data);
76
- }
77
- }
78
-
79
- _createOrUpdateElement(id: string, data: RendererData): void {
80
- const prevElement: ?Element = this._idToElement.get(id);
81
- const nextElement: Element = {
82
- id,
83
- key: data.key,
84
- displayName: data.displayName,
85
- children: [],
86
- type: data.type,
87
- };
88
-
89
- this._idToElement.set(id, nextElement);
90
-
91
- data.children.forEach(childFiber => {
92
- if (childFiber !== null) {
93
- this._crawl(nextElement, this._getId(childFiber));
94
- }
95
- });
96
-
97
- if (prevElement == null) {
98
- debug('emit("mount")', id, nextElement);
99
- this.emit('mount', nextElement);
100
- } else if (!areElementsEqual(prevElement, nextElement)) {
101
- debug('emit("update")', id, nextElement);
102
- this.emit('update', nextElement);
103
- }
104
- }
105
-
106
- onHookMount = ({
107
- data,
108
- fiber,
109
- renderer,
110
- }: {
111
- data: RendererData,
112
- fiber: Fiber,
113
- renderer: RendererID,
114
- }) => {
115
- const id = this._getId(fiber);
116
-
117
- this._idToRendererData.set(id, data);
118
- this._idToRendererID.set(id, renderer);
35
+ onHookDisplayNames = (displayNames: Map<number, string>) => {
36
+ debug('onHookDisplayNames', displayNames);
37
+ this._bridge.send('displayNames', displayNames);
38
};
39
121
- onHookRootCommitted = ({
122
- data,
123
- fiber,
124
- renderer,
125
- }: {
126
- data: RendererData,
127
- fiber: Fiber,
128
- renderer: RendererID,
129
- }) => {
130
- const id = this._getId(fiber);
131
-
132
- // TODO: Can we use the effects list on update for a faster path?
133
- // Mounts (Placements) and unmounts (Deletions) are on the child; need to update parent children in that case.
134
- this._createOrUpdateElement(id, data);
135
-
136
- if (!this._roots.has(id)) {
137
- this._roots.add(id);
138
- debug('emit("root")', id);
139
- this.emit('root', id);
140
- }
141
-
142
- this.emit('rootCommitted', id);
40
+ onHookKeys = (keys: Map<number, string>) => {
41
+ debug('onHookKeys', keys);
42
+ this._bridge.send('keys', keys);
43
};
44
145
- onHookUnmount = ({ fiber }: { fiber: Fiber }) => {
146
- const id = this._getId(fiber);
147
-
148
- if (this._roots.has(id)) {
149
- this._roots.delete(id);
150
- this.emit('rootUnmounted', id);
151
- }
152
-
153
- if (this._idToElement.has(id)) {
154
- this._idToElement.delete(id);
155
- debug('emit("unmount")', id);
156
- this.emit('unmount', id);
157
- }
158
-
159
- this._fiberToID.delete(fiber);
160
- this._idToRendererData.delete(id);
161
- this._idToRendererID.delete(id);
45
+ onHookOperations = (operations: Uint32Array) => {
46
+ debug('onHookOperations', operations);
47
+ this._bridge.send('operations', operations);
48
};
49
164
- onHookUpdate = ({ data, fiber }: { data: RendererData, fiber: Fiber }) => {
165
- const id = this._getId(fiber);
166
-
167
- this._idToRendererData.set(id, data);
50
+ onHookRootCommitted = (rootID: string) => {
51
+ debug('onHookRootCommitted', rootID);
52
+ this._bridge.send('rootCommitted', rootID);
53
};
54
}
170
-
171
-function areElementsEqual(
172
- prevElement: ?Element,
173
- nextElement: ?Element
174
-): boolean {
175
- if (!prevElement || !nextElement) {
176
- return false;
177
- }
178
-
179
- if (
180
- prevElement.key !== nextElement.key ||
181
- prevElement.displayName !== nextElement.displayName ||
182
- prevElement.children.length !== nextElement.children.length ||
183
- prevElement.type !== nextElement.type
184
- ) {
185
- return false;
186
- }
187
-
188
- for (let i = 0; i < prevElement.children.length; i++) {
189
- if (prevElement.children[i] !== nextElement.children[i]) {
190
- return false;
191
- }
192
- }
193
-
194
- return true;
195
-}
src/backend/index.js
+1
-3
@@ -12,10 +12,8 @@ export function initBackend(hook: Hook, agent: Agent): void {
12
rendererInterface.walkTree();
13
}),
14
15
+ hook.sub('operations', agent.onHookOperations),
16
hook.sub('rootCommitted', agent.onHookRootCommitted),
16
- hook.sub('mount', agent.onHookMount),
17
- hook.sub('update', agent.onHookUpdate),
18
- hook.sub('unmount', agent.onHookUnmount),
17
18
// TODO Add additional subscriptions required for profiling mode
19
];
src/backend/renderer.js
+262
-173
@@ -8,21 +8,25 @@ import {
8
ElementTypeMemo,
9
ElementTypeOtherOrUnknown,
10
ElementTypeProfiler,
11
+ ElementTypeRoot,
12
ElementTypeSuspense,
13
} from 'src/devtools/types';
14
+import { utfEncodeString } from '../utils';
15
import { getDisplayName } from './utils';
16
+import {
17
+ TREE_OPERATION_ADD,
18
+ TREE_OPERATION_REMOVE,
19
+ TREE_OPERATION_RESET_CHILDREN,
20
+} from '../constants';
21
22
import type {
23
Fiber,
24
Hook,
25
ReactRenderer,
19
- RendererData,
26
+ FiberData,
27
RendererInterface,
28
} from './types';
29
23
-// TODO: If we're profiling, process update
24
-// TODO: Throttle process update to app tree
25
-
30
function getInternalReactConstants(version) {
31
const ReactSymbols = {
32
CONCURRENT_MODE_NUMBER: 0xeacf,
@@ -184,10 +188,61 @@ export function attach(
188
189
const primaryFibers: WeakSet<Fiber> = new WeakSet();
190
187
- // TODO: we might want to change the data structure
188
- // once we no longer suppport Stack versions of `getData`.
191
+ // Keep this function in sync with getDataForFiber()
192
+ function shouldFilterFiber(fiber: Fiber): boolean {
193
+ const { type, tag } = fiber;
194
+
195
+ switch (tag) {
196
+ case ClassComponent:
197
+ case FunctionComponent:
198
+ case IncompleteClassComponent:
199
+ case IndeterminateComponent:
200
+ case ForwardRef:
201
+ case HostRoot:
202
+ case MemoComponent:
203
+ case SimpleMemoComponent:
204
+ return false;
205
+ case HostPortal:
206
+ case HostComponent:
207
+ case HostText:
208
+ case Fragment:
209
+ return true;
210
+ default:
211
+ const symbolOrNumber =
212
+ typeof type === 'object' && type !== null ? type.$$typeof : type;
213
+
214
+ const switchValue =
215
+ // $FlowFixMe facebook/flow/issues/2362
216
+ typeof symbolOrNumber === 'symbol'
217
+ ? symbolOrNumber.toString()
218
+ : symbolOrNumber;
219
+
220
+ switch (switchValue) {
221
+ case CONCURRENT_MODE_NUMBER:
222
+ case CONCURRENT_MODE_SYMBOL_STRING:
223
+ case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
224
+ case STRICT_MODE_NUMBER:
225
+ case STRICT_MODE_SYMBOL_STRING:
226
+ return true;
227
+ case CONTEXT_PROVIDER_NUMBER:
228
+ case CONTEXT_PROVIDER_SYMBOL_STRING:
229
+ case CONTEXT_CONSUMER_NUMBER:
230
+ case CONTEXT_CONSUMER_SYMBOL_STRING:
231
+ case SUSPENSE_NUMBER:
232
+ case SUSPENSE_SYMBOL_STRING:
233
+ case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
234
+ case PROFILER_NUMBER:
235
+ case PROFILER_SYMBOL_STRING:
236
+ return false;
237
+ default:
238
+ return false;
239
+ }
240
+ }
241
+ }
242
+
243
+ // TODO: we might want to change the data structure once we no longer suppport Stack versions of `getData`.
244
// TODO: Keep in sync with getElementType()
190
- function getRendererDataFromFiber(fiber: Fiber): RendererData {
245
+ function getDataForFiber(fiber: Fiber): FiberData {
246
const { elementType, type, key, tag } = fiber;
247
248
// This is to support lazy components with a Promise as the type.
@@ -199,11 +254,7 @@ export function attach(
254
}
255
}
256
202
- // Suspense has some special behavior when timed out that we have to handle.
203
- // see https://github.com/facebook/react/pull/13823
204
- let isTimedOutSuspense: boolean = false;
205
-
206
- let rendererData: RendererData = ((null: any): RendererData);
257
+ let fiberData: FiberData = ((null: any): FiberData);
258
let displayName: string = ((null: any): string);
259
let resolvedContext: any = null;
260
@@ -212,8 +263,7 @@ export function attach(
263
case FunctionComponent:
264
case IncompleteClassComponent:
265
case IndeterminateComponent:
215
- rendererData = {
216
- children: [],
266
+ fiberData = {
267
displayName: getDisplayName(resolvedType),
268
key,
269
type: ElementTypeClassOrFunction,
@@ -224,26 +274,28 @@ export function attach(
274
displayName =
275
resolvedType.displayName ||
276
(functionName !== '' ? `ForwardRef(${functionName})` : 'ForwardRef');
227
- rendererData = {
228
- children: [],
277
+
278
+ fiberData = {
279
displayName,
280
key,
281
type: ElementTypeForwardRef,
282
};
283
break;
284
case HostRoot:
285
+ return {
286
+ displayName: null,
287
+ key: null,
288
+ type: ElementTypeRoot,
289
+ };
290
case HostPortal:
291
case HostComponent:
292
case HostText:
293
case Fragment:
239
- // Not displayed in Elements tree
240
- rendererData = {
241
- children: [],
294
+ return {
295
displayName: null,
243
- key,
296
+ key: null,
297
type: ElementTypeOtherOrUnknown,
298
};
246
- break;
299
case MemoComponent:
300
case SimpleMemoComponent:
301
if (elementType.displayName) {
@@ -252,8 +304,7 @@ export function attach(
304
displayName = type.displayName || type.name;
305
displayName = displayName ? `Memo(${displayName})` : 'Memo';
306
}
255
- rendererData = {
256
- children: [],
307
+ fiberData = {
308
displayName,
309
key,
310
type: ElementTypeMemo,
@@ -273,14 +324,11 @@ export function attach(
324
case CONCURRENT_MODE_NUMBER:
325
case CONCURRENT_MODE_SYMBOL_STRING:
326
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
276
- // Not displayed in Elements tree
277
- rendererData = {
278
- children: [],
327
+ return {
328
displayName: null,
280
- key,
329
+ key: null,
330
type: ElementTypeOtherOrUnknown,
331
};
283
- break;
332
case CONTEXT_PROVIDER_NUMBER:
333
case CONTEXT_PROVIDER_SYMBOL_STRING:
334
// 16.3.0 exposed the context object as "context"
@@ -289,8 +337,7 @@ export function attach(
337
displayName = `${resolvedContext.displayName ||
338
'Context'}.Provider`;
339
292
- rendererData = {
293
- children: [],
340
+ fiberData = {
341
displayName,
342
key,
343
type: ElementTypeContext,
@@ -307,8 +354,7 @@ export function attach(
354
displayName = `${resolvedContext.displayName ||
355
'Context'}.Consumer`;
356
310
- rendererData = {
311
- children: [],
357
+ fiberData = {
358
displayName,
359
key,
360
type: ElementTypeContext,
@@ -316,8 +362,7 @@ export function attach(
362
break;
363
case STRICT_MODE_NUMBER:
364
case STRICT_MODE_SYMBOL_STRING:
319
- rendererData = {
320
- children: [],
365
+ fiberData = {
366
displayName: null,
367
key,
368
type: ElementTypeOtherOrUnknown,
@@ -326,11 +371,7 @@ export function attach(
371
case SUSPENSE_NUMBER:
372
case SUSPENSE_SYMBOL_STRING:
373
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
329
- // Suspense components only have a non-null memoizedState if they're timed-out.
330
- isTimedOutSuspense = fiber.memoizedState !== null;
331
-
332
- rendererData = {
333
- children: [],
374
+ fiberData = {
375
displayName: 'Suspense',
376
key,
377
type: ElementTypeSuspense,
@@ -338,8 +379,7 @@ export function attach(
379
break;
380
case PROFILER_NUMBER:
381
case PROFILER_SYMBOL_STRING:
341
- rendererData = {
342
- children: [],
382
+ fiberData = {
383
displayName: `Profiler(${fiber.memoizedProps.id})`,
384
key,
385
type: ElementTypeProfiler,
@@ -348,8 +388,7 @@ export function attach(
388
default:
389
// Unknown element type.
390
// This may mean a new element type that has not yet been added to DevTools.
351
- rendererData = {
352
- children: [],
391
+ fiberData = {
392
displayName: null,
393
key,
394
type: ElementTypeOtherOrUnknown,
@@ -359,30 +398,7 @@ export function attach(
398
break;
399
}
400
362
- const { children } = rendererData;
363
- if (isTimedOutSuspense) {
364
- // The behavior of timed-out Suspense trees is unique.
365
- // Rather than unmount the timed out content (and possibly lose important state),
366
- // React re-parents this content within a hidden Fragment while the fallback is showing.
367
- // This behavior doesn't need to be observable in the DevTools though.
368
- // It might even result in a bad user experience for e.g. node selection in the Elements panel.
369
- // The easiest fix is to strip out the intermediate Fragment fibers,
370
- // so the Elements panel and Profiler don't need to special case them.
371
- const primaryChildFragment = fiber.child;
372
- const primaryChild = primaryChildFragment.child;
373
- const fallbackChildFragment = primaryChildFragment.sibling;
374
- const fallbackChild = fallbackChildFragment.child;
375
- children.push(primaryChild);
376
- children.push(fallbackChild);
377
- } else {
378
- let child = fiber.child;
379
- while (child) {
380
- children.push(getPrimaryFiber(child));
381
- child = child.sibling;
382
- }
383
- }
384
-
385
- return rendererData;
401
+ return fiberData;
402
}
403
404
// This is a slightly annoying indirection.
@@ -401,6 +417,17 @@ export function attach(
417
return fiber;
418
}
419
420
+ let uidCounter: number = 0;
421
+ const fiberToIDMap: WeakMap<Fiber, number> = new WeakMap();
422
+
423
+ function getFiberID(primaryFiber: Fiber): number {
424
+ if (!fiberToIDMap.has(primaryFiber)) {
425
+ fiberToIDMap.set(primaryFiber, ++uidCounter);
426
+ }
427
+ return ((fiberToIDMap.get(primaryFiber): any): number);
428
+ }
429
+
430
+ // eslint-disable-next-line no-unused-vars
431
function hasDataChanged(prevFiber: Fiber, nextFiber: Fiber): boolean {
432
switch (nextFiber.tag) {
433
case ClassComponent:
@@ -425,6 +452,7 @@ export function attach(
452
}
453
}
454
455
+ // eslint-disable-next-line no-unused-vars
456
function haveProfilerTimesChanged(
457
prevFiber: Fiber,
458
nextFiber: Fiber
@@ -437,127 +465,176 @@ export function attach(
465
);
466
}
467
440
- let pendingEvents = [];
441
-
442
- // TODO: Do we still need to send events like this?
443
- function flushPendingEvents() {
444
- const events = pendingEvents;
445
- pendingEvents = [];
446
- for (let i = 0; i < events.length; i++) {
447
- const event = events[i];
448
- hook.emit(event.type, event);
468
+ let pendingOperations: Uint32Array = new Uint32Array(0);
469
+
470
+ function addOperation(
471
+ newAction: Uint32Array,
472
+ addToStartOfQueue: boolean = false
473
+ ): void {
474
+ const oldActions = pendingOperations;
475
+ pendingOperations = new Uint32Array(oldActions.length + newAction.length);
476
+ if (addToStartOfQueue) {
477
+ pendingOperations.set(newAction);
478
+ pendingOperations.set(oldActions, newAction.length);
479
+ } else {
480
+ pendingOperations.set(oldActions);
481
+ pendingOperations.set(newAction, oldActions.length);
482
}
483
}
484
452
- function enqueueMount(fiber) {
453
- pendingEvents.push({
454
- fiber: getPrimaryFiber(fiber),
455
- data: getRendererDataFromFiber(fiber),
456
- renderer: rendererID,
457
- type: 'mount',
458
- });
485
+ function flushPendingEvents(root: Object): void {
486
+ // Let the frontend know about tree operations.
487
+ hook.emit('operations', pendingOperations);
488
+ pendingOperations = new Uint32Array(0);
489
490
+ // Let the frontend know that we're done working on this root.
491
+ // Technically this could be inferred, but it's better to explicitly do this for the case of multi roots.
492
+ // Else the frontend would need to traverse the tree to identify which updates corresponded to which roots.
493
+ hook.emit('rootCommitted', getFiberID(getPrimaryFiber(root.current)));
494
+ }
495
+
496
+ function enqueueMount(fiber: Fiber, parentFiber: Fiber | null) {
497
const isRoot = fiber.tag === HostRoot;
498
+ const id = getFiberID(getPrimaryFiber(fiber));
499
+
500
if (isRoot) {
462
- pendingEvents.push({
463
- fiber: getPrimaryFiber(fiber),
464
- renderer: rendererID,
465
- type: 'root',
466
- });
467
- }
468
- }
501
+ const operation = new Uint32Array(4);
502
+ operation[0] = TREE_OPERATION_ADD;
503
+ operation[1] = id;
504
+ operation[2] = ElementTypeRoot;
505
+ operation[3] = 0; // Identifies this fiber as a root
506
+ addOperation(operation);
507
+ } else {
508
+ const { displayName, key, type } = getDataForFiber(fiber);
509
470
- function enqueueUpdateIfNecessary(fiber, hasChildOrderChanged) {
471
- const data = getRendererDataFromFiber(fiber);
472
-
473
- if (!hasChildOrderChanged && !hasDataChanged(fiber.alternate, fiber)) {
474
- // If only timing information has changed, we still need to update the nodes.
475
- // But we can do it in a faster way since we know it's safe to skip the children.
476
- // It's also important to avoid emitting an "update" signal for the node in this case,
477
- // Since that would indicate to the Profiler that it was part of the "commit" when it wasn't.
478
- if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
479
- pendingEvents.push({
480
- fiber: getPrimaryFiber(fiber),
481
- data,
482
- renderer: rendererID,
483
- type: 'updateProfileTimes',
484
- });
510
+ let encodedDisplayName = ((null: any): Uint8Array);
511
+ let encodedKey = ((null: any): Uint8Array);
512
+
513
+ if (displayName !== null) {
514
+ encodedDisplayName = utfEncodeString(displayName);
515
}
486
- return;
516
+
517
+ if (key !== null) {
518
+ if (typeof key === 'number') {
519
+ encodedKey = new Uint8Array(1);
520
+ encodedKey[0] = key;
521
+ } else {
522
+ encodedKey = utfEncodeString(key);
523
+ }
524
+ }
525
+
526
+ const encodedDisplayNameSize =
527
+ displayName === null ? 0 : encodedDisplayName.length;
528
+ const encodedKeySize = key === null ? 0 : encodedKey.length;
529
+
530
+ const operation = new Uint32Array(
531
+ 6 + encodedDisplayNameSize + encodedKeySize
532
+ );
533
+ operation[0] = TREE_OPERATION_ADD;
534
+ operation[1] = id;
535
+ operation[2] = type;
536
+ operation[3] = getFiberID(getPrimaryFiber(parentFiber));
537
+ operation[4] = encodedDisplayNameSize;
538
+ if (displayName !== null) {
539
+ operation.set(encodedDisplayName, 5);
540
+ }
541
+ operation[5 + encodedDisplayNameSize] = encodedKeySize;
542
+ if (key !== null) {
543
+ operation.set(encodedKey, 5 + encodedDisplayNameSize + 1);
544
+ }
545
+ addOperation(operation);
546
}
488
- pendingEvents.push({
489
- fiber: getPrimaryFiber(fiber),
490
- data,
491
- renderer: rendererID,
492
- type: 'update',
493
- });
547
}
548
549
function enqueueUnmount(fiber) {
550
const isRoot = fiber.tag === HostRoot;
551
const primaryFiber = getPrimaryFiber(fiber);
499
- const event = {
500
- fiber: primaryFiber,
501
- renderer: rendererID,
502
- type: 'unmount',
503
- };
552
if (isRoot) {
505
- pendingEvents.push(event);
506
- } else {
553
+ const id = getFiberID(getPrimaryFiber(fiber));
554
+ const operation = new Uint32Array(2);
555
+ operation[0] = TREE_OPERATION_REMOVE;
556
+ operation[1] = id;
557
+ addOperation(operation);
558
+ } else if (!shouldFilterFiber(fiber)) {
559
// Non-root fibers are deleted during the commit phase.
560
// They are deleted in the child-first order. However
561
// DevTools currently expects deletions to be parent-first.
562
// This is why we unshift deletions rather than push them.
511
- pendingEvents.unshift(event);
563
+ const id = getFiberID(getPrimaryFiber(fiber));
564
+ const operation = new Uint32Array(2);
565
+ operation[0] = TREE_OPERATION_REMOVE;
566
+ operation[1] = id;
567
+ addOperation(operation, true);
568
}
569
primaryFibers.delete(primaryFiber);
570
+ fiberToIDMap.delete(primaryFiber);
571
}
572
516
- function markRootCommitted(fiber) {
517
- pendingEvents.push({
518
- fiber: getPrimaryFiber(fiber),
519
- data: getRendererDataFromFiber(fiber),
520
- renderer: rendererID,
521
- type: 'rootCommitted',
522
- });
573
+ function mountFiber(fiber: Fiber, parentFiber: Fiber | null) {
574
+ const shouldEnqueueMount = !shouldFilterFiber(fiber);
575
+
576
+ if (shouldEnqueueMount) {
577
+ enqueueMount(fiber, parentFiber);
578
+ }
579
+
580
+ if (fiber.child !== null) {
581
+ mountFiber(fiber.child, shouldEnqueueMount ? fiber : parentFiber);
582
+ }
583
+
584
+ if (fiber.sibling) {
585
+ mountFiber(fiber.sibling, parentFiber);
586
+ }
587
}
588
525
- function mountFiber(fiber) {
526
- // Depth-first.
527
- // Logs mounting of children first, parents later.
528
- let node = fiber;
529
- outer: while (true) {
530
- if (node.child) {
531
- node.child.return = node;
532
- node = node.child;
533
- continue;
534
- }
535
- enqueueMount(node);
536
- if (node === fiber) {
537
- return;
538
- }
539
- if (node.sibling) {
540
- node.sibling.return = node.return;
541
- node = node.sibling;
542
- continue;
589
+ function enqueueUpdateIfNecessary(
590
+ fiber: Fiber,
591
+ hasChildOrderChanged: boolean
592
+ ) {
593
+ // The frontend only really cares about the displayName, key, and children.
594
+ // The first two don't really change, so we are only concerned with the order of children here.
595
+ // This is trickier than a simple comparison though, since certain types of fibers are filtered.
596
+ if (hasChildOrderChanged) {
597
+ const nextChildren: Array<number> = [];
598
+
599
+ // This is a naive implimentation that shallowly recurses children.
600
+ // We might want to revisit this if it proves to be too inefficient.
601
+ let child = fiber.child;
602
+ while (child !== null) {
603
+ findReorderedChildren(child, nextChildren);
604
+ child = child.sibling;
605
}
544
- while (node.return) {
545
- node = node.return;
546
- enqueueMount(node);
547
- if (node === fiber) {
548
- return;
549
- }
550
- if (node.sibling) {
551
- node.sibling.return = node.return;
552
- node = node.sibling;
553
- continue outer;
554
- }
606
+
607
+ const numChildren = nextChildren.length;
608
+ const operation = new Uint32Array(3 + numChildren);
609
+ operation[0] = TREE_OPERATION_RESET_CHILDREN;
610
+ operation[1] = getFiberID(getPrimaryFiber(fiber));
611
+ operation[2] = numChildren;
612
+ operation.set(nextChildren, 3);
613
+ addOperation(operation);
614
+ }
615
+
616
+ // TODO (profiling) If we're profiling, also check to see if that data has changed.
617
+ }
618
+
619
+ function findReorderedChildren(fiber: Fiber, nextChildren: Array<number>) {
620
+ if (!shouldFilterFiber(fiber)) {
621
+ nextChildren.push(getFiberID(getPrimaryFiber(fiber)));
622
+ } else {
623
+ let child = fiber.child;
624
+ while (child !== null) {
625
+ findReorderedChildren(child, nextChildren);
626
+ child = child.sibling;
627
}
556
- return;
628
}
629
}
630
560
- function updateFiber(nextFiber, prevFiber) {
631
+ function updateFiber(
632
+ nextFiber: Fiber,
633
+ prevFiber: Fiber,
634
+ parentFiber: Fiber | null
635
+ ) {
636
+ const shouldEnqueueUpdate = !shouldFilterFiber(nextFiber);
637
+
638
// Suspense components only have a non-null memoizedState if they're timed-out.
639
const isTimedOutSuspense =
640
nextFiber.tag === ReactTypeOfWork.SuspenseComponent &&
@@ -574,15 +651,19 @@ export function attach(
651
const primaryChildFragment = nextFiber.child;
652
const fallbackChildFragment = primaryChildFragment.sibling;
653
const fallbackChild = fallbackChildFragment.child;
654
+
655
// The primary, hidden child is never actually updated in this case,
656
// so we can skip any updates to its tree.
657
// We only need to track updates to the Fallback UI for now.
658
if (fallbackChild.alternate) {
581
- updateFiber(fallbackChild, fallbackChild.alternate);
659
+ updateFiber(fallbackChild, fallbackChild.alternate, nextFiber);
660
} else {
583
- mountFiber(fallbackChild);
661
+ mountFiber(fallbackChild, nextFiber);
662
+ }
663
+
664
+ if (shouldEnqueueUpdate) {
665
+ enqueueUpdateIfNecessary(nextFiber, false);
666
}
585
- enqueueUpdateIfNecessary(nextFiber, false);
667
} else {
668
let hasChildOrderChanged = false;
669
if (nextFiber.child !== prevFiber.child) {
@@ -597,7 +678,11 @@ export function attach(
678
// We don't track deletions here because they are reported separately.
679
if (nextChild.alternate) {
680
const prevChild = nextChild.alternate;
600
- updateFiber(nextChild, prevChild);
681
+ updateFiber(
682
+ nextChild,
683
+ prevChild,
684
+ shouldEnqueueUpdate ? nextFiber : parentFiber
685
+ );
686
// However we also keep track if the order of the children matches
687
// the previous order. They are always different referentially, but
688
// if the instances line up conceptually we'll want to know that.
@@ -605,7 +690,10 @@ export function attach(
690
hasChildOrderChanged = true;
691
}
692
} else {
608
- mountFiber(nextChild);
693
+ mountFiber(
694
+ nextChild,
695
+ shouldEnqueueUpdate ? nextFiber : parentFiber
696
+ );
697
if (!hasChildOrderChanged) {
698
hasChildOrderChanged = true;
699
}
@@ -623,7 +711,10 @@ export function attach(
711
hasChildOrderChanged = true;
712
}
713
}
626
- enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
714
+
715
+ if (shouldEnqueueUpdate) {
716
+ enqueueUpdateIfNecessary(nextFiber, hasChildOrderChanged);
717
+ }
718
}
719
}
720
@@ -632,12 +723,11 @@ export function attach(
723
}
724
725
function walkTree() {
726
+ // Hydrate all the roots for the first time.
727
hook.getFiberRoots(rendererID).forEach(root => {
636
- // Hydrate all the roots for the first time.
637
- mountFiber(root.current);
638
- markRootCommitted(root.current);
728
+ mountFiber(root.current, null);
729
+ flushPendingEvents(root);
730
});
640
- flushPendingEvents();
731
}
732
733
function handleCommitFiberUnmount(fiber) {
@@ -660,21 +750,20 @@ export function attach(
750
current.memoizedState != null && current.memoizedState.element != null;
751
if (!wasMounted && isMounted) {
752
// Mount a new root.
663
- mountFiber(current);
753
+ mountFiber(current, null);
754
} else if (wasMounted && isMounted) {
755
// Update an existing root.
666
- updateFiber(current, alternate);
756
+ updateFiber(current, alternate, null);
757
} else if (wasMounted && !isMounted) {
758
// Unmount an existing root.
759
enqueueUnmount(current);
760
}
761
} else {
762
// Mount a new root.
673
- mountFiber(current);
763
+ mountFiber(current, null);
764
}
675
- markRootCommitted(current);
765
// We're done here.
677
- flushPendingEvents();
766
+ flushPendingEvents(root);
767
}
768
769
// The naming is confusing.
src/backend/types.js
+1
-2
@@ -12,10 +12,9 @@ export type Fiber = Object;
12
// TODO: If it's useful for the frontend to know which types of data an Element has
13
// (e.g. props, state, context, hooks) then we could add a bitmask field for this
14
// to keep the number of attributes small.
15
-export type RendererData = {|
15
+export type FiberData = {|
16
key: React$Key | null,
17
displayName: string | null,
18
- children: Array<Fiber>,
18
type: ElementType,
19
|};
20
src/constants.js
new
+5
@@ -0,0 +1,5 @@
1
+// @flow
2
+
3
+export const TREE_OPERATION_ADD = 1;
4
+export const TREE_OPERATION_REMOVE = 2;
5
+export const TREE_OPERATION_RESET_CHILDREN = 3;
src/devtools/store.js
+194
-200
@@ -1,73 +1,83 @@
1
// @flow
2
3
import EventEmitter from 'events';
4
-
5
-import type { Element, ElementTreeMetadata } from './types';
4
+import {
5
+ TREE_OPERATION_ADD,
6
+ TREE_OPERATION_REMOVE,
7
+ TREE_OPERATION_RESET_CHILDREN,
8
+} from '../constants';
9
+import { utfDecodeString } from '../utils';
10
+
11
+import type { Element, ElementType } from './types';
12
import type { Bridge } from '../types';
13
14
const debug = (methodName, ...args) => {
9
- console.log(`%cStore %c${methodName}`, 'color: red; font-weight: bold;', 'font-weight: bold;', ...args);
15
+ console.log(
16
+ `%cStore %c${methodName}`,
17
+ 'color: green; font-weight: bold;',
18
+ 'font-weight: bold;',
19
+ ...args
20
+ );
21
};
22
23
/**
24
* The store is the single source of truth for updates from the backend.
25
* ContextProviders can subscribe to the Store for specific things they want to provide.
26
*/
16
-class Store extends EventEmitter {
17
- _elementToElementTreeMetadata: WeakMap<
18
- Element,
19
- ElementTreeMetadata
20
- > = new WeakMap();
21
- _idToElement: Map<string, Element> = new Map();
22
- _idToParentID: Map<string, string> = new Map();
23
- _pendingDeletions: Set<string> = new Set();
27
+export default class Store extends EventEmitter {
28
+ // TODO Should items in this map be read-only for easier props comparison?
29
+ _idToElement: Map<number, Element> = new Map();
30
31
// Total number of visible elements (within all roots).
32
// Used for windowing purposes.
27
- numElements: number = 0;
33
+ _numElements: number = 0;
34
35
// This Array must be treated as immutable!
36
// Passive effects will check it for changes between render and mount.
31
- roots: $ReadOnlyArray<string> = [];
37
+ _roots: $ReadOnlyArray<number> = [];
38
39
constructor(bridge: Bridge) {
40
super();
41
36
- bridge.on('root', this.onBridgeRoot);
42
+ bridge.on('operations', this.onBridgeOperations);
43
bridge.on('rootCommitted', this.onBridgeRootCommitted);
44
+ }
45
+
46
+ get numElements(): number {
47
+ return this._numElements;
48
+ }
49
39
- bridge.on('mount', this.onBridgeMount);
40
- bridge.on('update', this.onBridgeUpdated);
41
- bridge.on('unmount', this.onBridgeUnmounted);
50
+ get roots(): $ReadOnlyArray<number> {
51
+ return this._roots;
52
}
53
44
- getElementAtIndex(index: number): Element | null {
54
+ getElementAtIndex(index: number): Element {
55
if (index < 0 || index >= this.numElements) {
46
- return null;
56
+ throw Error(`Invalid index ${index} specified`);
57
}
58
59
+ // Find wich root this element is in...
60
let rootID;
61
let root;
51
-
62
let rootWeight = 0;
53
- for (let i = 0; i < this.roots.length; i++) {
54
- rootID = this.roots[i];
55
- root = this._idToElement.get(rootID);
56
- const { weight } = this._elementToElementTreeMetadata.get(root);
57
-
58
- if (rootWeight + weight > index) {
63
+ for (let i = 0; i < this._roots.length; i++) {
64
+ rootID = this._roots[i];
65
+ root = ((this._idToElement.get(rootID): any): Element);
66
+ if (rootWeight + root.weight > index) {
67
break;
68
+ } else {
69
+ rootWeight += root.weight;
70
}
71
}
72
63
- let currentElement = root;
73
+ // Crawl the tree to find the correct root...
74
+ let currentElement = ((root: any): Element);
75
let currentWeight = 0;
65
-
76
while (index !== currentWeight) {
77
for (let i = 0; i < currentElement.children.length; i++) {
78
const childID = currentElement.children[i];
69
- const child = this._idToElement.get(childID);
70
- const { weight } = this._elementToElementTreeMetadata.get(child);
79
+ const child = ((this._idToElement.get(childID): any): Element);
80
+ const { weight } = child;
81
if (index <= currentWeight + weight) {
82
currentWeight++;
83
currentElement = child;
@@ -78,207 +88,191 @@ class Store extends EventEmitter {
88
}
89
}
90
81
- return currentElement;
82
- }
83
-
84
- getElementByID(id: string) {
85
- return this._idToElement.get(id);
86
- }
87
-
88
- geParent(id: string) {
89
- return this._idToParentID.get(id);
90
- }
91
-
92
- getTreeMetadataForElement(element: Element) {
93
- return this._elementToElementTreeMetadata.get(element);
91
+ return ((currentElement: any): Element);
92
}
93
96
- _crawlForTreeMetadata(id: string, depth: number = 0): number {
97
- let weight = 1;
98
-
94
+ getElementByID(id: number): Element {
95
const element = this._idToElement.get(id);
96
101
- // TODO: Figure out why sometimes items aren't being sent across the bridge.
102
- // It always seems to be one of the ListItems...
97
if (element == null) {
104
- console.log(`%cNo element found for id "${id}"`, 'background-color: yellow; font-weight: bold;');
105
- return 0;
98
+ throw Error(`No element found with id "${id}`);
99
}
100
108
- element.children.forEach(childID => {
109
- weight += this._crawlForTreeMetadata(childID, depth + 1);
110
- });
111
-
112
- this._elementToElementTreeMetadata.set(element, {
113
- depth,
114
- weight,
115
- });
116
-
117
- return weight;
101
+ return ((element: any): Element);
102
}
103
120
- _updateElementTreeMetadata(prevElement: Element, element: Element): void {
121
- if (prevElement.children === element.children) {
122
- return;
123
- }
124
-
125
- // Compare children in case they have changed.
126
- // For each child that was removed, we need to shrink the list by this many elements.
127
- // For each child that was added, we need to grow the list by this many elements.
128
-
129
- const prevChildren = prevElement.children;
130
- const prevNumChildren = prevChildren.length;
131
-
132
- const children = element.children;
133
- const numChildren = children.length;
134
-
135
- // TODO: The below diffing could be optimized more.
136
-
137
- // Scan for deletions
138
- for (let i = 0; i < prevNumChildren; i++) {
139
- const childID = prevChildren[i];
140
- if (!children.includes(childID)) {
141
- const child = this._idToElement.get(childID);
142
- const { weight } = this._elementToElementTreeMetadata.get(child);
143
-
144
- this.numElements -= weight;
145
-
146
- let current = element;
147
- while (current !== null) {
148
- const datum = this._elementToElementTreeMetadata.get(current);
149
- datum.weight -= weight;
104
+ onBridgeOperations = (operations: Uint32Array) => {
105
+ debug('onBridgeOperations', operations);
106
+
107
+ let haveRootsChanged = false;
108
+
109
+ let i = 0;
110
+ while (i < operations.length) {
111
+ let id: number = ((null: any): number);
112
+ let element: Element = ((null: any): Element);
113
+ let parentID: number = ((null: any): number);
114
+ let parentElement: Element = ((null: any): Element);
115
+ let type: ElementType = ((null: any): ElementType);
116
+ let weightDelta: number = 0;
117
+
118
+ const operation = operations[i];
119
+
120
+ switch (operation) {
121
+ case TREE_OPERATION_ADD:
122
+ id = ((operations[i + 1]: any): number);
123
+ type = ((operations[i + 2]: any): ElementType);
124
+ parentID = ((operations[i + 3]: any): number);
125
+
126
+ i = i + 4;
127
+
128
+ if (parentID === 0) {
129
+ debug('Add', `new root fiber ${id}`);
130
+
131
+ this._roots = this._roots.concat(id);
132
+
133
+ this._idToElement.set(id, {
134
+ children: [],
135
+ depth: 0,
136
+ displayName: null,
137
+ id,
138
+ key: null,
139
+ parentID: 0,
140
+ type,
141
+ weight: 1,
142
+ });
143
+
144
+ haveRootsChanged = true;
145
+ } else {
146
+ const displayNameLength = operations[i];
147
+ i++;
148
+ const displayName =
149
+ displayNameLength === 0
150
+ ? null
151
+ : utfDecodeString(
152
+ (operations.slice(i, i + displayNameLength): any)
153
+ );
154
+ i += displayNameLength;
155
+
156
+ const keyLength = operations[i];
157
+ i++;
158
+ const key =
159
+ keyLength === 0
160
+ ? null
161
+ : utfDecodeString((operations.slice(i, i + keyLength): any));
162
+ i += +keyLength;
163
+
164
+ debug('Add', `fiber ${id}, type ${type}, as child of ${parentID}`);
165
+
166
+ // TODO Fix this; there should not be duplicate "ADD" operations for a given element.
167
+ if (this._idToElement.has(id)) {
168
+ console.warn(
169
+ `fiber ${id}, type ${type}, already added as child of ${parentID}`
170
+ );
171
+ } else {
172
+ parentElement = ((this._idToElement.get(parentID): any): Element);
173
+ parentElement.children = parentElement.children.concat(id);
174
+
175
+ this._idToElement.set(id, {
176
+ children: [],
177
+ depth: parentElement.depth + 1,
178
+ displayName,
179
+ id,
180
+ key,
181
+ parentID: parentElement.id,
182
+ type,
183
+ weight: 1,
184
+ });
185
+
186
+ weightDelta = 1;
187
+ }
188
+ }
189
+ break;
190
+ case TREE_OPERATION_REMOVE:
191
+ id = ((operations[i + 1]: any): number);
192
151
- const parent = this._idToElement.get(datum.parentID);
152
- current =
153
- parent != null
154
- ? this._elementToElementTreeMetadata.get(parent)
155
- : null;
156
- }
157
- }
158
- }
193
+ i = i + 2;
194
160
- // Scan for additions
161
- for (let i = 0; i < numChildren; i++) {
162
- const childID = children[i];
163
- if (!prevChildren.includes(childID)) {
164
- const child = this._idToElement.get(childID);
165
- const { depth } = this._elementToElementTreeMetadata.get(element);
166
- const weight = this._crawlForTreeMetadata(
167
- childID,
168
- depth + 1
169
- );
170
-
171
- this.numElements += weight;
172
-
173
- let current = element;
174
- while (current !== null) {
175
- const datum = this._elementToElementTreeMetadata.get(current);
176
- datum.weight += weight;
177
-
178
- const parent = this._idToElement.get(datum.parentID);
179
- current =
180
- parent != null
181
- ? this._elementToElementTreeMetadata.get(parent)
182
- : null;
183
- }
184
- }
185
- }
186
- }
195
+ debug('Remove', `fiber ${id} from tree`);
196
188
-__printTree() {
189
- let i = 0;
190
- this.roots.forEach(rootID => {
191
- const root = this._idToElement.get(rootID);
192
- const { weight } = this._elementToElementTreeMetadata.get(root);
193
- for (let j = i; j < i + weight; j++) {
194
- const element = this.getElementAtIndex(j)
195
- const { depth } = this._elementToElementTreeMetadata.get(element);
197
+ element = ((this._idToElement.get(id): any): Element);
198
+ parentID = element.parentID;
199
197
- console.log(' '.repeat(depth) + element.displayName);
198
- }
199
- i += weight;
200
- });
201
-}
200
+ weightDelta = -element.weight;
201
203
- onBridgeMount = (element: Element) => {
204
- const { id } = element;
205
- debug('onBridgeMount()', element);
206
- this._idToElement.set(id, element);
202
+ this._idToElement.delete(id);
203
208
- element.children.forEach(childID => {
209
- this._idToParentID.set(childID, id);
210
- });
204
+ parentElement = ((this._idToElement.get(parentID): any): Element);
205
+ if (parentElement != null) {
206
+ parentElement.children = parentElement.children.filter(
207
+ childID => childID !== id
208
+ );
209
+ }
210
+ break;
211
+ case TREE_OPERATION_RESET_CHILDREN:
212
+ id = ((operations[i + 1]: any): number);
213
+ const numChildren = ((operations[i + 2]: any): number);
214
+ const children = ((operations.slice(
215
+ i + 3,
216
+ i + 3 + numChildren
217
+ ): any): Array<number>);
218
212
- this.emit(id);
213
- };
219
+ i = i + 3 + numChildren;
220
215
- onBridgeRoot = (id: string) => {
216
- debug('onBridgeRoot()', id);
217
- if (!this.roots.includes(id)) {
218
- this.roots = this.roots.concat(id);
221
+ debug('Re-order', `fiber ${id} children ${children.join(',')}`);
222
220
- // Generate tree metadata used for windowing.
221
- this.numElements += this._crawlForTreeMetadata(id);
223
+ element = ((this._idToElement.get(id): any): Element);
224
+ element.children = children;
225
223
- this.emit('roots');
224
- }
225
- };
226
+ const prevWeight = element.weight;
227
+ let childWeight = 0;
228
227
- onBridgeRootCommitted = (rootID: string) => {
228
- this._pendingDeletions.forEach(id => {
229
- this._idToElement.delete(id);
229
+ children.forEach(childID => {
230
+ const child = ((this._idToElement.get(childID): any): Element);
231
+ childWeight += child.weight;
232
+ });
233
231
- if (this._idToParentID.has(id)) {
232
- this._idToParentID.delete(id);
234
+ element.weight = childWeight + 1;
235
+ weightDelta = childWeight + 1 - prevWeight;
236
+ break;
237
+ default:
238
+ throw Error(`Unsupported Bridge operation ${operation}`);
239
}
234
- });
235
- this._pendingDeletions.clear();
236
-
237
- debug('onBridgeRootCommitted()', rootID);
238
- this.emit('rootCommitted', rootID);
239
-
240
-this.__printTree();
241
- };
242
-
243
- // TODO: Unmounting removes id-to-element before crawling, which breaks it.
244
- // Should I just ditch the idea of a WeakMap in favor of an explicit it-to-metadata mapping like with parents?
245
- onBridgeUnmounted = (id: string) => {
246
- debug('onBridgeUnmounted()', id);
247
- this._pendingDeletions.add(id);
240
249
- const index = this.roots.indexOf(id);
250
- if (index >= 0) {
251
- this.roots = this.roots
252
- .slice(0, index)
253
- .concat(this.roots.slice(index + 1));
241
+ this._numElements += weightDelta;
242
255
- const root = this._idToElement.get(id);
256
- const {weight} = this._elementToElementTreeMetadata.get(root);
257
-
258
- this.numElements -= weight;
243
+ while (parentElement != null) {
244
+ parentElement.weight += weightDelta;
245
+ parentElement = ((this._idToElement.get(
246
+ parentElement.parentID
247
+ ): any): Element);
248
+ }
249
+ }
250
251
+ if (haveRootsChanged) {
252
this.emit('roots');
253
}
254
};
255
264
- onBridgeUpdated = (element: Element) => {
265
- const { id } = element;
266
- debug('onBridgeUpdated()', element);
256
+ onBridgeRootCommitted = (rootID: number) => {
257
+ debug('onBridgeRootCommitted', rootID);
258
268
- const prevElement = ((this._idToElement.get(id): any): Element);
269
- const prevElementTreeMetadata = ((this._elementToElementTreeMetadata.get(prevElement): any): ElementTreeMetadata);
259
+ this.emit('rootCommitted');
260
271
- this._idToElement.set(id, element);
272
- this._elementToElementTreeMetadata.set(
273
- element,
274
- prevElementTreeMetadata
275
- );
276
-
277
- // Update tree metadata used for windowing.
278
- this._updateElementTreeMetadata(prevElement, element);
261
+ // this.__printTree(rootID);
262
+ };
263
280
- this.emit(id);
264
+ // DEBUG
265
+ __printTree = (rootID: number) => {
266
+ const printElement = (id: number) => {
267
+ const element = ((this._idToElement.get(id): any): Element);
268
+ console.log(
269
+ `${' '.repeat(element.depth)}${element.id}:${element.displayName ||
270
+ ''}${element.key ? `key:"${element.key}"` : ''} (${element.weight})`
271
+ );
272
+ element.children.forEach(printElement);
273
+ };
274
+ const root = ((this._idToElement.get(rootID): any): Element);
275
+ console.log('printing root:', rootID);
276
+ root.children.forEach(printElement);
277
};
278
}
283
-
284
-export default Store;
src/devtools/types.js
+13
-15
@@ -1,6 +1,6 @@
1
// @flow
2
3
-import typeof Store from './Store';
3
+import Store from './Store';
4
5
export const ElementTypeClassOrFunction = 1;
6
export const ElementTypeContext = 2;
@@ -8,28 +8,26 @@ export const ElementTypeForwardRef = 3;
8
export const ElementTypeMemo = 4;
9
export const ElementTypeOtherOrUnknown = 5;
10
export const ElementTypeProfiler = 6;
11
-export const ElementTypeSuspense = 7;
11
+export const ElementTypeRoot = 7;
12
+export const ElementTypeSuspense = 8;
13
13
-export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7;
14
+export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
15
16
// TODO: Add profiling node
17
18
export type Element = {|
18
- id: string,
19
- type: ElementType,
20
- key: React$Key | null,
21
- displayName: string | null,
22
- children: Array<string>,
23
-|};
24
-
25
-export type ElementTreeMetadata = {|
19
+ children: Array<number>,
20
depth: number,
27
- rootID: string,
21
+ displayName: string | null,
22
+ id: number,
23
+ key: number | string | null,
24
+ parentID: number,
25
+ type: ElementType,
26
weight: number,
27
|};
28
29
export type InspectedElement = {|
32
- id: string,
30
+ id: number,
31
context: Object | null,
32
hooks: Object | null,
33
props: Object | null,
@@ -38,7 +36,7 @@ export type InspectedElement = {|
36
source: Object,
37
|};
38
41
-export type TreeContext = {|
39
+export type TreeMetadataType = {|
40
size: number,
41
store: Store,
44
-|};
\ No newline at end of file
42
+|};
src/devtools/views/Element.js
+5
-8
@@ -5,13 +5,13 @@ import { TreeContext } from './contexts';
5
6
import styles from './Element.css';
7
8
-type Props = {|
8
+type Props = {
9
index: number,
10
style: Object,
11
-|};
11
+};
12
13
export default function Element({ index, style }: Props) {
14
- const {store} = useContext(TreeContext);
14
+ const { store } = useContext(TreeContext);
15
const element = store.getElementAtIndex(index);
16
17
// DevTools are rendered in concurrent mode.
@@ -22,10 +22,7 @@ export default function Element({ index, style }: Props) {
22
return null;
23
}
24
25
- const elementTreeMetadata = store.getTreeMetadataForElement(element);
26
-
27
- const { children, displayName, key } = element;
28
- const { depth } = elementTreeMetadata;
25
+ const { children, depth, displayName, key } = element;
26
27
// TODO: Add state for toggling element open/close
28
@@ -34,7 +31,7 @@ export default function Element({ index, style }: Props) {
31
className={styles.Element}
32
style={{
33
...style,
37
- paddingLeft: `${1 + depth}rem`
34
+ paddingLeft: `${1 + depth}rem`,
35
}}
36
>
37
{children.length > 0 && <span className={styles.ArrowOpen} />}
src/devtools/views/Elements.js
+3
-3
@@ -15,12 +15,12 @@ export type Props = {|
15
|};
16
17
export default function Elements({ bridge, browserName, themeName }: Props) {
18
- const store = useMemo(() => new Store(bridge), []);
18
+ const store = useMemo<Store>(() => new Store(bridge), []);
19
20
- const [treeContext, setTreeContext] = useState(({
20
+ const [treeContext, setTreeContext] = useState({
21
size: store.numElements,
22
store,
23
- }));
23
+ });
24
25
useLayoutEffect(() => {
26
const handler = () => {
src/devtools/views/contexts.js
+4
-2
@@ -2,10 +2,12 @@
2
3
import { createContext } from 'react';
4
5
-import type { TreeMetadata } from '../types';
5
+import type { TreeMetadataType } from '../types';
6
7
import Store from '../store';
8
9
export const RootsContext = createContext<Array<string>>([]);
10
export const StoreContext = createContext<Store>(((null: any): Store));
11
-export const TreeContext = createContext<TreeMetadata>(((null: any): TreeMetadata));
\ No newline at end of file
11
+export const TreeContext = createContext<TreeMetadataType>(
12
+ ((null: any): TreeMetadataType)
13
+);
src/devtools/views/hooks.js
+6
-6
@@ -2,14 +2,14 @@
2
3
import { useLayoutEffect, useState } from 'react';
4
5
-import type { Element, ElementTreeMetadata } from '../types';
5
+import type { Element } from '../types';
6
import Store from '../store';
7
8
// TODO useEffect has a bug where sometimes subscriptions don't get cleaned up correctly.
9
// Potentially related to github.com/facebookincubator/redux-react-hook/issues/17
10
// As a temporary work around, switch back to layout effect.
11
12
-export function useElement(store: Store, id: string): ?Element {
12
+export function useElement(store: Store, id: number): ?Element {
13
const [element, setElement] = useState<?Element>(store.getElementByID(id));
14
15
useLayoutEffect(() => {
@@ -17,7 +17,7 @@ export function useElement(store: Store, id: string): ?Element {
17
setElement(((store.getElementByID(id): any): Element));
18
19
// Listen for changes to the element.
20
- store.addListener(id, handler);
20
+ store.addListener((id: any), handler);
21
22
// Check for changes that may have happened between render and mount.
23
const newElement = store.getElementByID(id);
@@ -26,14 +26,14 @@ export function useElement(store: Store, id: string): ?Element {
26
}
27
28
// Remove event listener on unmount.
29
- return () => store.removeListener(id, handler);
29
+ return () => store.removeListener((id: any), handler);
30
}, [store, id]);
31
32
return element;
33
}
34
35
-export function useRoots(store: Store): $ReadOnlyArray<string> {
36
- const [roots, setRoots] = useState<$ReadOnlyArray<string>>(store.roots);
35
+export function useRoots(store: Store): $ReadOnlyArray<number> {
36
+ const [roots, setRoots] = useState<$ReadOnlyArray<number>>(store.roots);
37
38
useLayoutEffect(() => {
39
const handler = () => setRoots(store.roots);
src/utils.js
new
+19
@@ -0,0 +1,19 @@
1
+// @flow
2
+
3
+export function utfDecodeString(array: Uint8Array): string {
4
+ let string = '';
5
+ const { length } = array;
6
+ for (let i = 0; i < length; i++) {
7
+ string += String.fromCharCode(array[i]);
8
+ }
9
+ return string;
10
+}
11
+
12
+export function utfEncodeString(string: string): Uint8Array {
13
+ const array = new Uint8Array(string.length);
14
+ const { length } = string;
15
+ for (let i = 0; i < length; i++) {
16
+ array[i] = string.charCodeAt(i);
17
+ }
18
+ return array;
19
+}
yarn.lock
+4
-4
@@ -8422,10 +8422,10 @@ react-virtualized-auto-sizer@^1.0.2:
8422
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
8423
integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
8424
8425
-react-window@^1.5.0:
8426
- version "1.5.0"
8427
- resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.5.0.tgz#3e56b720b97666bce38a9e932bdd238d56e258f1"
8428
- integrity sha512-55WeZKjMNF5JdCuKghc/H65DBecoeGgH8MOX3CgT7BJ66xb4ITRuXPUlz0qU6r50wetdF/oLhorYBRvKD4Z1IQ==
8425
+react-window@^1.5.1:
8426
+ version "1.5.1"
8427
+ resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.5.1.tgz#9d68624d5ba58ddf331321fb74b0a75184523d16"
8428
+ integrity sha512-D855yW104ek8+gSLW8G6Dt69WnhSlGXR4Ld+IoZW4aQg6a1pt7cZLb+fcOugVDVgl4RfrLSfg9aLBnvS7zsOaw==
8429
dependencies:
8430
"@babel/runtime" "^7.0.0"
8431
memoize-one "^3.1.1"