Rewrite to make it sturdier
Dan Abramov committed
Jun 7, 2019 at 17:21 UTC
9243bc194c240d3f06ccd5e4333f3488f2628e2f
6 files changed
+396
-531
src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap
+71
-1
@@ -1,6 +1,6 @@
1
// Jest Snapshot v1, https://goo.gl/fbAQLP
2
3
-exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = `
3
+exports[`Store (legacy) collapseNodesByDefault:false should not filter DOM nodes from the store tree: 1: mount 1`] = `
4
[root]
5
▾ <Grandparent>
6
▾ <div>
@@ -13,8 +13,27 @@ exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes fro
13
▾ <div>
14
▾ <Child>
15
<div>
16
+ <Nothing>
17
`;
18
19
+exports[`Store (legacy) collapseNodesByDefault:false should not filter DOM nodes from the store tree: 2: update 1`] = `
20
+[root]
21
+ ▾ <Grandparent>
22
+ ▾ <div>
23
+ ▾ <div>
24
+ ▾ <Parent>
25
+ ▾ <div>
26
+ ▾ <Child>
27
+ <div>
28
+ ▾ <Parent>
29
+ ▾ <div>
30
+ ▾ <Child>
31
+ <div>
32
+ <Nothing>
33
+`;
34
+
35
+exports[`Store (legacy) collapseNodesByDefault:false should not filter DOM nodes from the store tree: 5: unmount 1`] = ``;
36
+
37
exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 1: mount 1`] = `
38
[root]
39
▾ <Root>
@@ -281,8 +300,20 @@ exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes
300
▾ <div>
301
▸ <div>
302
▸ <Parent>
303
+ <Nothing>
304
`;
305
306
+exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 4: final update 1`] = `
307
+[root]
308
+ ▾ <Grandparent>
309
+ ▾ <div>
310
+ ▸ <div>
311
+ ▸ <Parent>
312
+ <Nothing>
313
+`;
314
+
315
+exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 5: unmount 1`] = ``;
316
+
317
exports[`Store (legacy) collapseNodesByDefault:true should support expanding deep parts of the tree: 1: mount 1`] = `
318
[root]
319
▸ <Wrapper>
@@ -422,6 +453,45 @@ exports[`Store (legacy) collapseNodesByDefault:true should support mount and upd
453
454
exports[`Store (legacy) collapseNodesByDefault:true should support mount and update operations: 3: unmount 1`] = ``;
455
456
+exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 1: mount 1`] = `
457
+[root]
458
+ ▸ <Root>
459
+`;
460
+
461
+exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 2: reorder children 1`] = `
462
+[root]
463
+ ▸ <Root>
464
+`;
465
+
466
+exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 3: expand root 1`] = `
467
+[root]
468
+ ▾ <Root>
469
+ ▸ <div>
470
+`;
471
+
472
+exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 4: expand div 1`] = `
473
+[root]
474
+ ▾ <Root>
475
+ ▾ <div>
476
+ ▸ <Bar key="bar">
477
+ ▸ <Foo key="foo">
478
+`;
479
+
480
+exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 4: expand leaves 1`] = `
481
+[root]
482
+ ▾ <Root>
483
+ ▾ <div>
484
+ ▾ <Bar key="bar">
485
+ ▸ <div>
486
+ ▾ <Foo key="foo">
487
+ ▸ <div>
488
+`;
489
+
490
+exports[`Store (legacy) collapseNodesByDefault:true should support reordering of children: 5: collapse root 1`] = `
491
+[root]
492
+ ▸ <Root>
493
+`;
494
+
495
exports[`Store (legacy) should not allow a root node to be collapsed: 1: mount 1`] = `
496
[root]
497
▸ <Component>
src/__tests__/legacy/storeLegacy-v15-test.js
+44
-19
@@ -107,29 +107,40 @@ describe('Store (legacy)', () => {
107
expect(store).toMatchSnapshot('4: unmount A');
108
});
109
110
- it('should filter DOM nodes from the store tree', () => {
111
- const Grandparent = () => (
110
+ it('should not filter DOM nodes from the store tree', () => {
111
+ const Grandparent = ({ flip }) => (
112
<div>
113
<div>
114
- <Parent />
114
+ <Parent flip={flip} />
115
</div>
116
- <Parent />
116
+ <Parent flip={flip} />
117
+ <Nothing />
118
</div>
119
);
119
- const Parent = () => (
120
+ const Parent = ({ flip }) => (
121
<div>
122
+ {flip ? 'foo' : null}
123
<Child />
124
+ {flip && [null, 'hello', 42]}
125
+ {flip ? 'bar' : 'baz'}
126
</div>
127
);
128
const Child = () => <div>Hi!</div>;
129
+ const Nothing = () => null;
130
131
+ const container = document.createElement('div');
132
act(() =>
127
- ReactDOM.render(
128
- <Grandparent count={4} />,
129
- document.createElement('div')
130
- )
133
+ ReactDOM.render(<Grandparent count={4} flip={false} />, container)
134
);
135
expect(store).toMatchSnapshot('1: mount');
136
+
137
+ act(() =>
138
+ ReactDOM.render(<Grandparent count={4} flip={true} />, container)
139
+ );
140
+ expect(store).toMatchSnapshot('2: update');
141
+
142
+ act(() => ReactDOM.unmountComponentAtNode(container));
143
+ expect(store).toMatchSnapshot('5: unmount');
144
});
145
146
it('should support collapsing parts of the tree', () => {
@@ -317,26 +328,29 @@ describe('Store (legacy)', () => {
328
});
329
330
it('should not filter DOM nodes from the store tree', () => {
320
- const Grandparent = () => (
331
+ const Grandparent = ({ flip }) => (
332
<div>
333
<div>
323
- <Parent />
334
+ <Parent flip={flip} />
335
</div>
325
- <Parent />
336
+ <Parent flip={flip} />
337
+ <Nothing />
338
</div>
339
);
328
- const Parent = () => (
340
+ const Parent = ({ flip }) => (
341
<div>
342
+ {flip ? 'foo' : null}
343
<Child />
344
+ {flip && [null, 'hello', 42]}
345
+ {flip ? 'bar' : 'baz'}
346
</div>
347
);
348
const Child = () => <div>Hi!</div>;
349
+ const Nothing = () => null;
350
351
+ const container = document.createElement('div');
352
act(() =>
336
- ReactDOM.render(
337
- <Grandparent count={4} />,
338
- document.createElement('div')
339
- )
353
+ ReactDOM.render(<Grandparent count={4} flip={false} />, container)
354
);
355
expect(store).toMatchSnapshot('1: mount');
356
@@ -345,6 +359,14 @@ describe('Store (legacy)', () => {
359
360
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
361
expect(store).toMatchSnapshot('3: expand div');
362
+
363
+ act(() =>
364
+ ReactDOM.render(<Grandparent count={4} flip={true} />, container)
365
+ );
366
+ expect(store).toMatchSnapshot('4: final update');
367
+
368
+ act(() => ReactDOM.unmountComponentAtNode(container));
369
+ expect(store).toMatchSnapshot('5: unmount');
370
});
371
372
it('should support expanding parts of the tree', () => {
@@ -445,7 +467,7 @@ describe('Store (legacy)', () => {
467
expect(store).toMatchSnapshot('6: expand middle node');
468
});
469
448
- xit('should support reordering of children', () => {
470
+ it('should support reordering of children', () => {
471
const Root = ({ children }) => <div>{children}</div>;
472
const Component = () => <div />;
473
@@ -467,9 +489,12 @@ describe('Store (legacy)', () => {
489
act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(0), false));
490
expect(store).toMatchSnapshot('3: expand root');
491
492
+ act(() => store.toggleIsCollapsed(store.getElementIDAtIndex(1), false));
493
+ expect(store).toMatchSnapshot('4: expand div');
494
+
495
act(() => {
496
+ store.toggleIsCollapsed(store.getElementIDAtIndex(3), false);
497
store.toggleIsCollapsed(store.getElementIDAtIndex(2), false);
472
- store.toggleIsCollapsed(store.getElementIDAtIndex(1), false);
498
});
499
expect(store).toMatchSnapshot('4: expand leaves');
500
src/backend/legacy/getChildren.js
deleted
-45
@@ -1,45 +0,0 @@
1
-// @flow
2
-
3
-// TODO (legacy) Respect component filters
4
-
5
-export default function getChildren(internalInstance: Object): Array<any> {
6
- let children = [];
7
-
8
- // If the parent is a native node without rendered children, but with
9
- // multiple string children, then the `element` that gets passed in here is
10
- // a plain value -- a string or number.
11
- if (typeof internalInstance !== 'object') {
12
- // No children
13
- } else if (
14
- internalInstance._currentElement === null ||
15
- internalInstance._currentElement === false
16
- ) {
17
- // No children
18
- } else if (internalInstance._renderedComponent) {
19
- children = [internalInstance._renderedComponent];
20
- } else if (internalInstance._renderedChildren) {
21
- children = renderedChildrenToArray(internalInstance._renderedChildren);
22
- }
23
- // Note: we skip the case where children are just strings or numbers
24
- // because the new DevTools skips over host text nodes anyway.
25
-
26
- const instance = internalInstance._instance;
27
- if (instance) {
28
- // TODO: React ART currently falls in this bucket, but this doesn't
29
- // actually make sense and we should clean this up after stabilizing our
30
- // API for backends
31
- if (instance._renderedChildren) {
32
- children = renderedChildrenToArray(instance._renderedChildren);
33
- }
34
- }
35
-
36
- return children;
37
-}
38
-
39
-function renderedChildrenToArray(renderedChildren): Array<any> {
40
- const childrenArray = [];
41
- for (let name in renderedChildren) {
42
- childrenArray.push(renderedChildren[name]);
43
- }
44
- return childrenArray;
45
-}
src/backend/legacy/getElementType.js
deleted
-25
@@ -1,25 +0,0 @@
1
-// @flow
2
-import {
3
- ElementTypeClass,
4
- ElementTypeHostComponent,
5
- ElementTypeOtherOrUnknown,
6
-} from 'src/types';
7
-
8
-import type { InternalInstance } from './renderer';
9
-import type { ElementType } from 'src/types';
10
-
11
-export default function getElementType(
12
- internalInstance: InternalInstance
13
-): ElementType {
14
- // != used deliberately here to catch undefined and null
15
- if (internalInstance._currentElement != null) {
16
- const elementType = internalInstance._currentElement.type;
17
- if (typeof elementType === 'function') {
18
- return ElementTypeClass;
19
- } else if (typeof elementType === 'string') {
20
- return ElementTypeHostComponent;
21
- }
22
- }
23
-
24
- return ElementTypeOtherOrUnknown;
25
-}
src/backend/legacy/renderer.js
+281
-427
@@ -4,6 +4,7 @@ import {
4
ElementTypeClass,
5
ElementTypeFunction,
6
ElementTypeRoot,
7
+ ElementTypeHostComponent,
8
ElementTypeOtherOrUnknown,
9
} from 'src/types';
10
import { getUID, utfEncodeString, printOperationsArray } from '../../utils';
@@ -14,15 +15,8 @@ import {
15
TREE_OPERATION_REMOVE,
16
TREE_OPERATION_REORDER_CHILDREN,
17
} from '../../constants';
17
-import getChildren from './getChildren';
18
import getData from './getData';
19
-import getElementType from './getElementType';
20
-import {
21
- decorateResult,
22
- decorateMany,
23
- forceUpdate,
24
- restoreMany,
25
-} from './utils';
19
+import { decorateMany, forceUpdate, restoreMany } from './utils';
20
21
import type {
22
DevToolsHook,
@@ -32,7 +26,7 @@ import type {
26
PathMatch,
27
RendererInterface,
28
} from '../types';
35
-import type { ComponentFilter } from 'src/types';
29
+import type { ComponentFilter, ElementType } from 'src/types';
30
import type {
31
InspectedElement,
32
Owner,
@@ -41,6 +35,51 @@ import type {
35
export type InternalInstance = Object;
36
type LegacyRenderer = Object;
37
38
+function getElementType(internalInstance: InternalInstance): ElementType {
39
+ // != used deliberately here to catch undefined and null
40
+ if (internalInstance._currentElement != null) {
41
+ const elementType = internalInstance._currentElement.type;
42
+ if (typeof elementType === 'function') {
43
+ return ElementTypeClass;
44
+ } else if (typeof elementType === 'string') {
45
+ return ElementTypeHostComponent;
46
+ }
47
+ }
48
+ return ElementTypeOtherOrUnknown;
49
+}
50
+
51
+function getChildren(internalInstance: Object): Array<any> {
52
+ let children = [];
53
+
54
+ // If the parent is a native node without rendered children, but with
55
+ // multiple string children, then the `element` that gets passed in here is
56
+ // a plain value -- a string or number.
57
+ if (typeof internalInstance !== 'object') {
58
+ // No children
59
+ } else if (
60
+ internalInstance._currentElement === null ||
61
+ internalInstance._currentElement === false
62
+ ) {
63
+ // No children
64
+ } else if (internalInstance._renderedComponent) {
65
+ const child = internalInstance._renderedComponent;
66
+ if (getElementType(child) !== ElementTypeOtherOrUnknown) {
67
+ children.push(child);
68
+ }
69
+ } else if (internalInstance._renderedChildren) {
70
+ const renderedChildren = internalInstance._renderedChildren;
71
+ for (let name in renderedChildren) {
72
+ const child = renderedChildren[name];
73
+ if (getElementType(child) !== ElementTypeOtherOrUnknown) {
74
+ children.push(child);
75
+ }
76
+ }
77
+ }
78
+ // Note: we skip the case where children are just strings or numbers
79
+ // because the new DevTools skips over host text nodes anyway.
80
+ return children;
81
+}
82
+
83
export function attach(
84
hook: DevToolsHook,
85
rendererID: number,
@@ -48,201 +87,186 @@ export function attach(
87
global: Object
88
): RendererInterface {
89
const idToInternalInstanceMap: Map<number, InternalInstance> = new Map();
51
- const idToParentIDMap: Map<number, number> = new Map();
90
const internalInstanceToIDMap: Map<InternalInstance, number> = new Map();
53
- const internalInstanceToLastKnownChildrenMap: WeakMap<
54
- InternalInstance,
55
- Array<number>
56
- > = new WeakMap();
57
- const rootIDs: Set<number> = new Set();
58
-
59
- function getID(internalInstance: InternalInstance): number {
60
- if (typeof internalInstance !== 'object') {
61
- throw new Error('Invalid internal instance: ' + internalInstance);
62
- }
63
- if (!internalInstanceToIDMap.has(internalInstance)) {
64
- const id = getUID();
65
- internalInstanceToIDMap.set(internalInstance, id);
66
- idToInternalInstanceMap.set(id, internalInstance);
67
- }
68
- return ((internalInstanceToIDMap.get(internalInstance): any): number);
69
- }
70
-
71
- function getChildIDs(internalInstance: InternalInstance): Array<number> {
72
- return getChildren(internalInstance).map(getID);
73
- }
74
-
75
- function findNearestAncestorInTree(
76
- internalInstance: InternalInstance
77
- ): number | null {
78
- let current = internalInstance;
79
- while (current != null) {
80
- const id = getID(current);
81
- if (
82
- rootIDs.has(id) ||
83
- getElementType(current) !== ElementTypeOtherOrUnknown
84
- ) {
85
- return id;
86
- }
87
- const parentID = idToParentIDMap.get(id);
88
- current = parentID != null ? idToInternalInstanceMap.get(parentID) : null;
89
- }
90
- return null;
91
- }
91
92
let getInternalIDForNative: GetFiberIDForNative = ((null: any): GetFiberIDForNative);
93
let findNativeNodeForInternalID: (id: number) => ?NativeType;
94
96
- // React Native
97
- if (renderer.Mount.findNodeHandle && renderer.Mount.nativeTagToRootNodeID) {
98
- getInternalIDForNative = (nativeTag, findNearestUnfilteredAncestor) => {
99
- const internalInstance = renderer.Mount.nativeTagToRootNodeID(nativeTag);
100
- return findNearestAncestorInTree(internalInstance);
101
- };
102
- findNativeNodeForInternalID = (id: number) => {
103
- const internalInstance = idToInternalInstanceMap.get(id);
104
- return renderer.Mount.findNodeHandle(internalInstance);
105
- };
106
-
107
- // React DOM 15+
108
- } else if (renderer.ComponentTree) {
95
+ if (renderer.ComponentTree) {
96
getInternalIDForNative = (node, findNearestUnfilteredAncestor) => {
97
const internalInstance = renderer.ComponentTree.getClosestInstanceFromNode(
98
node
99
);
113
- return findNearestAncestorInTree(internalInstance);
100
+ return internalInstanceToIDMap.get(internalInstance) || null;
101
};
102
findNativeNodeForInternalID = (id: number) => {
103
const internalInstance = idToInternalInstanceMap.get(id);
104
return renderer.ComponentTree.getNodeFromInstance(internalInstance);
105
};
119
-
120
- // React DOM
106
} else if (renderer.Mount.getID && renderer.Mount.getNode) {
107
getInternalIDForNative = (node, findNearestUnfilteredAncestor) => {
123
- let id = renderer.Mount.getID(node);
124
- while (node && node.parentNode && !id) {
125
- node = node.parentNode;
126
- id = renderer.Mount.getID(node);
127
- }
128
- return id;
108
+ // Not implemented.
109
+ return null;
110
};
130
-
111
findNativeNodeForInternalID = (id: number) => {
132
- try {
133
- const internalInstance = idToInternalInstanceMap.get(id);
134
- if (internalInstance != null) {
135
- return renderer.Mount.getNode(internalInstance._rootNodeID);
136
- }
137
- } catch (e) {}
138
-
112
+ // Not implemented.
113
return null;
114
};
141
- } else {
142
- console.warn(
143
- 'Unknown React version (does not have getID), probably an unshimmed React Native'
144
- );
115
}
116
147
- let oldReconcilerMethods = null;
148
- let oldRenderComponent = null;
149
- let oldRenderRoot = null;
150
-
151
- // React DOM
152
- if (renderer.Mount._renderNewRootComponent) {
153
- oldRenderRoot = decorateResult(
154
- renderer.Mount,
155
- '_renderNewRootComponent',
156
- internalInstance => {
157
- // TODO: we might need to reset currentParentID before this runs.
158
-
159
- const id = getID(internalInstance);
160
- rootIDs.add(id);
161
-
162
- if (__DEBUG__) {
163
- console.log('renderer.Mount._renderNewRootComponent()', id);
164
- }
165
-
166
- // TODO: maybe we need to record this mount.
167
- // Needs testing.
168
-
169
- // If we're mounting a root, we've just finished a batch of work,
170
- // so it's safe to synchronously flush.
171
- flushPendingEvents(id);
172
- }
173
- );
174
-
175
- // React Native
176
- } else if (renderer.Mount.renderComponent) {
177
- oldRenderComponent = decorateResult(
178
- renderer.Mount,
179
- 'renderComponent',
180
- internalInstance => {
181
- // TODO: we might need to reset currentParentID before this runs.
182
-
183
- const id = getID(internalInstance);
184
- rootIDs.add(id);
185
-
186
- if (__DEBUG__) {
187
- console.log('renderer.Mount.renderComponent()', id);
188
- }
189
-
190
- // TODO: maybe we need to record this mount.
191
- // Needs testing.
117
+ function getID(internalInstance: InternalInstance): number {
118
+ if (typeof internalInstance !== 'object') {
119
+ throw new Error('Invalid internal instance: ' + internalInstance);
120
+ }
121
+ if (!internalInstanceToIDMap.has(internalInstance)) {
122
+ const id = getUID();
123
+ internalInstanceToIDMap.set(internalInstance, id);
124
+ idToInternalInstanceMap.set(id, internalInstance);
125
+ }
126
+ return ((internalInstanceToIDMap.get(internalInstance): any): number);
127
+ }
128
193
- // If we're mounting a root, we've just finished a batch of work,
194
- // so it's safe to synchronously flush.
195
- flushPendingEvents(id);
129
+ function areEqualArrays(a, b) {
130
+ if (a.length !== b.length) {
131
+ return false;
132
+ }
133
+ for (let i = 0; i < a.length; i++) {
134
+ if (a[i] !== b[i]) {
135
+ return false;
136
}
197
- );
137
+ }
138
+ return true;
139
}
140
141
// This is shared mutable state that lets us keep track of where we are.
201
- let currentParentID = 0;
142
+ let parentIDStack = [];
143
144
+ let oldReconcilerMethods = null;
145
if (renderer.Reconciler) {
146
oldReconcilerMethods = decorateMany(renderer.Reconciler, {
147
mountComponent(fn, args) {
148
const [internalInstance] = args;
149
+ if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
150
+ return fn.apply(this, args);
151
+ }
152
208
- recordPendingMount(internalInstance);
209
-
210
- let prevParentID = currentParentID;
211
- currentParentID = getID(internalInstance);
212
- const result = fn.apply(this, args);
213
- currentParentID = prevParentID;
153
+ const id = getID(internalInstance);
154
215
- return result;
155
+ // Push the operation.
156
+ const parentID =
157
+ parentIDStack.length > 0
158
+ ? parentIDStack[parentIDStack.length - 1]
159
+ : 0;
160
+ recordMount(internalInstance, id, parentID);
161
+ parentIDStack.push(id);
162
+
163
+ try {
164
+ const result = fn.apply(this, args);
165
+ parentIDStack.pop();
166
+ return result;
167
+ } catch (err) {
168
+ parentIDStack = [];
169
+ throw err;
170
+ } finally {
171
+ if (parentIDStack.length === 0) {
172
+ flushPendingEvents(id);
173
+ }
174
+ }
175
},
176
performUpdateIfNecessary(fn, args) {
177
const [internalInstance] = args;
178
+ if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
179
+ return fn.apply(this, args);
180
+ }
181
220
- let prevParentID = currentParentID;
221
- currentParentID = getID(internalInstance);
222
- const result = fn.apply(this, args);
223
- currentParentID = prevParentID;
182
+ const id = getID(internalInstance);
183
+ parentIDStack.push(id);
184
225
- recordPendingReorder(internalInstance);
226
- return result;
185
+ const prevChildren = getChildren(internalInstance);
186
+ try {
187
+ const result = fn.apply(this, args);
188
+
189
+ const nextChildren = getChildren(internalInstance);
190
+ if (!areEqualArrays(prevChildren, nextChildren)) {
191
+ // Push the operation
192
+ recordReorder(internalInstance, id, nextChildren);
193
+ }
194
+
195
+ parentIDStack.pop();
196
+ return result;
197
+ } catch (err) {
198
+ parentIDStack = [];
199
+ throw err;
200
+ } finally {
201
+ if (parentIDStack.length === 0) {
202
+ // TODO: this is probably wrong!
203
+ const rootID =
204
+ internalInstance._topLevelWrapper === null
205
+ ? id
206
+ : getID(internalInstance._topLevelWrapper);
207
+ flushPendingEvents(rootID);
208
+ }
209
+ }
210
},
211
receiveComponent(fn, args) {
212
const [internalInstance] = args;
213
+ if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
214
+ return fn.apply(this, args);
215
+ }
216
+
217
+ const id = getID(internalInstance);
218
+ parentIDStack.push(id);
219
231
- let prevParentID = currentParentID;
232
- currentParentID = getID(internalInstance);
233
- const result = fn.apply(this, args);
234
- currentParentID = prevParentID;
220
+ const prevChildren = getChildren(internalInstance);
221
+ try {
222
+ const result = fn.apply(this, args);
223
236
- recordPendingReorder(internalInstance);
237
- return result;
224
+ const nextChildren = getChildren(internalInstance);
225
+ if (!areEqualArrays(prevChildren, nextChildren)) {
226
+ // Push the operation
227
+ recordReorder(internalInstance, id, nextChildren);
228
+ }
229
+
230
+ parentIDStack.pop();
231
+ return result;
232
+ } catch (err) {
233
+ parentIDStack = [];
234
+ throw err;
235
+ } finally {
236
+ if (parentIDStack.length === 0) {
237
+ // TODO: this is probably wrong!
238
+ const rootID =
239
+ internalInstance._topLevelWrapper === null
240
+ ? id
241
+ : getID(internalInstance._topLevelWrapper);
242
+ flushPendingEvents(rootID);
243
+ }
244
+ }
245
},
246
unmountComponent(fn, args) {
247
const [internalInstance] = args;
248
+ if (getElementType(internalInstance) === ElementTypeOtherOrUnknown) {
249
+ return fn.apply(this, args);
250
+ }
251
242
- const result = fn.apply(this, args);
243
-
244
- recordPendingUnmount(internalInstance);
245
- return result;
252
+ const id = getID(internalInstance);
253
+ parentIDStack.push(id);
254
+ try {
255
+ const result = fn.apply(this, args);
256
+ parentIDStack.pop();
257
+
258
+ // Push the operation.
259
+ recordUnmount(internalInstance, id);
260
+
261
+ return result;
262
+ } catch (err) {
263
+ parentIDStack = [];
264
+ throw err;
265
+ } finally {
266
+ if (parentIDStack.length === 0) {
267
+ flushPendingEvents(id);
268
+ }
269
+ }
270
},
271
});
272
}
@@ -255,110 +279,87 @@ export function attach(
279
restoreMany(renderer.Reconciler, oldReconcilerMethods);
280
}
281
}
258
- if (oldRenderRoot !== null) {
259
- renderer.Mount._renderNewRootComponent = oldRenderRoot;
260
- }
261
- if (oldRenderComponent !== null) {
262
- renderer.Mount.renderComponent = oldRenderComponent;
263
- }
282
oldReconcilerMethods = null;
265
- oldRenderRoot = null;
266
- oldRenderComponent = null;
283
}
284
269
- const pendingMountIDs: Set<number> = new Set();
270
- const pendingUnmountIDs: Set<number> = new Set();
271
- const pendingReorderIDs: Set<number> = new Set();
272
- const pendingOperations: Array<number> = [];
273
- const pendingStringTable: Map<string, number> = new Map();
274
- let pendingStringTableLength: number = 0;
275
- let pendingUnmountedRootID: number | null = null;
276
-
277
- function pushOperation(op: number): void {
278
- if (__DEV__) {
279
- if (!Number.isInteger(op)) {
280
- console.error(
281
- 'pushOperation() was called but the value is not an integer.',
282
- op
283
- );
284
- }
285
- }
286
- pendingOperations.push(op);
287
- }
285
+ function recordMount(
286
+ internalInstance: InternalInstance,
287
+ id: number,
288
+ parentID: number
289
+ ) {
290
+ const isRoot = parentID === 0;
291
289
- // TODO Rethink the below queueing mechanism.
290
- // Every mount is some parent's update (except for the root mount which we can explicitly handle)
291
- // So maybe we only need to call queueFlushPendingEvents() for updates,
292
- // and maybe we can rely on an id-to-root Map for this case, to limit the scope of what we crawl.
293
-
294
- // Older React renderers did not have the concept of a commit.
295
- // The data structure was just ad-hoc mutated in place.
296
- // So except for the case of the root mounting the first time,
297
- // there is no event we can observe to signal that a render is finished.
298
- // However since older renderers were always synchronous,
299
- // we can use setTimeout to batch operations together.
300
- // In the case of a cascading update, we might batch multiple "commits"-
301
- // but that should be okay, since the batching is not strictly necessary.
302
- let flushPendingEventsTimeoutID: TimeoutID | null = null;
303
- function queueFlushPendingEvents() {
304
- if (flushPendingEventsTimeoutID === null) {
305
- flushPendingEventsTimeoutID = setTimeout(() => {
306
- flushPendingEventsTimeoutID = null;
307
-
308
- // If there are pending operations, walk the tree and find them.
309
- // Ideally we wouldjust pluck the pending operations out of the sets directly,
310
- // but without doing a full traversal, it would be hard for us to determine the filtered parent.
311
- // It should be possible to improve this though, by maintaining a map of id-to-parent,
312
- // and crawling upward to the first non-filtered node.
313
- // TODO Revisit this and think about it more...
314
- if (
315
- pendingMountIDs.size > 0 ||
316
- pendingUnmountIDs.size > 0 ||
317
- pendingReorderIDs.size > 0
318
- ) {
319
- rootIDs.forEach(flushPendingEvents);
320
- }
321
- }, 0);
292
+ if (__DEBUG__) {
293
+ console.log(
294
+ '%crecordMount()',
295
+ 'color: green; font-weight: bold;',
296
+ id,
297
+ getData(internalInstance).displayName
298
+ );
299
}
323
- }
300
325
- function flushInitialOperations() {
326
- // Older versions of React do not support profiling mode, so there's nothing to flush.
327
- // Crawl roots though and register any nodes that mounted before we were injected.
328
-
329
- const roots =
330
- renderer.Mount._instancesByReactRootID ||
331
- renderer.Mount._instancesByContainerID;
301
+ if (isRoot) {
302
+ // TODO Is this right? For all versions?
303
+ const hasOwnerMetadata =
304
+ internalInstance._currentElement != null &&
305
+ internalInstance._currentElement._owner != null;
306
333
- for (let key in roots) {
334
- const internalInstance = roots[key];
335
- const id = getID(internalInstance);
307
+ pushOperation(TREE_OPERATION_ADD);
308
+ pushOperation(id);
309
+ pushOperation(ElementTypeRoot);
310
+ pushOperation(0); // isProfilingSupported?
311
+ pushOperation(hasOwnerMetadata ? 1 : 0);
312
+ } else {
313
+ const { displayName, key, type } = getData(internalInstance);
314
337
- rootIDs.add(id);
315
+ const ownerID =
316
+ internalInstance._currentElement != null &&
317
+ internalInstance._currentElement._owner != null
318
+ ? getID(internalInstance._currentElement._owner)
319
+ : 0;
320
339
- crawlAndRecordInitialMounts(id, 0);
321
+ let displayNameStringID = getStringID(displayName);
322
+ let keyStringID = getStringID(key);
323
+ pushOperation(TREE_OPERATION_ADD);
324
+ pushOperation(id);
325
+ pushOperation(type);
326
+ pushOperation(parentID);
327
+ pushOperation(ownerID);
328
+ pushOperation(displayNameStringID);
329
+ pushOperation(keyStringID);
330
+ }
331
+ }
332
341
- // It's safe to synchronously flush for the root we just crawled.
342
- flushPendingEvents(id);
333
+ function recordReorder(
334
+ internalInstance: InternalInstance,
335
+ id: number,
336
+ nextChildren: Array<InternalInstance>
337
+ ) {
338
+ pushOperation(TREE_OPERATION_REORDER_CHILDREN);
339
+ pushOperation(id);
340
+ const nextChildIDs = nextChildren.map(getID);
341
+ pushOperation(nextChildIDs.length);
342
+ for (let i = 0; i < nextChildIDs.length; i++) {
343
+ pushOperation(nextChildIDs[i]);
344
}
345
}
346
346
- // TODO: this isn't covered by tests.
347
- // Might be broken.
347
+ function recordUnmount(internalInstance: InternalInstance, id: number) {
348
+ pendingUnmountedIDs.push(id);
349
+ internalInstanceToIDMap.delete(internalInstance);
350
+ idToInternalInstanceMap.delete(id);
351
+ }
352
+
353
function crawlAndRecordInitialMounts(id: number, parentID: number) {
354
const internalInstance = idToInternalInstanceMap.get(id);
355
351
- // Not all nodes are mounted in the frontend DevTools tree,
352
- // but it's important to track parent info even for the unmounted ones.
353
- idToParentIDMap.set(id, parentID);
354
-
356
if (__DEBUG__) {
357
console.group('crawlAndRecordInitialMounts() id:', id);
358
}
359
359
- recordMount(id, parentID);
360
- getChildIDs(internalInstance).forEach(childID =>
361
- crawlAndRecordInitialMounts(childID, id)
360
+ recordMount(internalInstance, id, parentID);
361
+ getChildren(internalInstance).forEach(child =>
362
+ crawlAndRecordInitialMounts(getID(child), id)
363
);
364
365
if (__DEBUG__) {
@@ -366,49 +367,39 @@ export function attach(
367
}
368
}
369
369
- function flushPendingEvents(rootID: number): void {
370
- // Record pending deletions.
371
- const unmountIDs = [];
372
- pendingUnmountIDs.forEach(id => {
373
- const internalInstance = idToInternalInstanceMap.get(id);
374
- const isRoot = rootIDs.has(id);
375
-
376
- if (__DEBUG__) {
377
- console.log(
378
- '%crecordUnmount()',
379
- 'color: red; font-weight: bold;',
380
- id,
381
- getData(internalInstance).displayName
382
- );
383
- }
370
+ function flushInitialOperations() {
371
+ // Crawl roots though and register any nodes that mounted before we were injected.
372
385
- // TODO: handle the case where it was never mounted.
386
- if (isRoot) {
387
- pendingUnmountedRootID = id;
388
- rootIDs.delete(id);
389
- } else {
390
- unmountIDs.push(id);
391
- }
373
+ const roots =
374
+ renderer.Mount._instancesByReactRootID ||
375
+ renderer.Mount._instancesByContainerID;
376
393
- idToInternalInstanceMap.delete(id);
394
- internalInstanceToIDMap.delete(internalInstance);
395
- });
377
+ for (let key in roots) {
378
+ const internalInstance = roots[key];
379
+ const id = getID(internalInstance);
380
+ crawlAndRecordInitialMounts(id, 0);
381
+ flushPendingEvents(id);
382
+ }
383
+ }
384
397
- pendingMountIDs.forEach(id => {
398
- if (pendingUnmountIDs.has(id)) {
399
- return;
400
- }
401
- const parentID = idToParentIDMap.get(id);
402
- if (parentID === undefined) {
403
- return;
404
- }
405
- recordMount(id, parentID);
406
- });
385
+ let pendingOperations: Array<number> = [];
386
+ let pendingStringTable: Map<string, number> = new Map();
387
+ let pendingUnmountedIDs: Array<number> = [];
388
+ let pendingStringTableLength: number = 0;
389
+ let pendingUnmountedRootID: number | null = null;
390
+
391
+ function flushPendingEvents(rootID: number) {
392
+ if (
393
+ pendingOperations.length === 0 &&
394
+ pendingUnmountedIDs.length === 0 &&
395
+ pendingUnmountedRootID === null
396
+ ) {
397
+ return;
398
+ }
399
400
const numUnmountIDs =
409
- unmountIDs.length + (pendingUnmountedRootID === null ? 0 : 1);
401
+ pendingUnmountedIDs.length + (pendingUnmountedRootID === null ? 0 : 1);
402
411
- const reorderOperations = computePendingReorderOperations();
403
const operations = new Uint32Array(
404
// Identify which renderer this update is coming from.
405
2 + // [rendererID, rootFiberID]
@@ -420,11 +411,7 @@ export function attach(
411
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
412
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
413
// Mount operations
423
- pendingOperations.length +
424
- // Reorder operation come last because
425
- // bridge expects them to not change children length.
426
- // So both mounts and unmounts need to have happened by now.
427
- reorderOperations.length
414
+ pendingOperations.length
415
);
416
417
// Identify which renderer this update is coming from.
@@ -449,8 +436,8 @@ export function attach(
436
// The first number is how many unmounted IDs we're gonna send.
437
operations[i++] = numUnmountIDs;
438
// Fill in the unmounts
452
- for (let j = 0; j < unmountIDs.length; j++) {
453
- operations[i++] = unmountIDs[j];
439
+ for (let j = 0; j < pendingUnmountedIDs.length; j++) {
440
+ operations[i++] = pendingUnmountedIDs[j];
441
}
442
// The root ID should always be unmounted last.
443
if (pendingUnmountedRootID !== null) {
@@ -463,8 +450,6 @@ export function attach(
450
operations.set(pendingOperations, i);
451
i += pendingOperations.length;
452
466
- operations.set(reorderOperations, i);
467
-
453
if (__DEBUG__) {
454
printOperationsArray(operations);
455
}
@@ -473,59 +458,22 @@ export function attach(
458
hook.emit('operations', operations);
459
460
pendingOperations.length = 0;
476
- pendingMountIDs.clear();
477
- pendingReorderIDs.clear();
478
- pendingUnmountIDs.clear();
461
+ pendingUnmountedIDs = [];
462
pendingUnmountedRootID = null;
463
pendingStringTable.clear();
464
pendingStringTableLength = 0;
465
}
466
484
- function computePendingReorderOperations(): Array<number> {
485
- const ops = [];
486
- pendingReorderIDs.forEach(id => {
487
- const internalInstance = idToInternalInstanceMap.get(id);
488
- if (internalInstance === undefined) {
489
- return;
490
- }
491
- const prevChildIDs = internalInstanceToLastKnownChildrenMap.get(
492
- internalInstance
493
- );
494
- const nextChildIDs = getChildIDs(internalInstance);
495
- internalInstanceToLastKnownChildrenMap.set(
496
- internalInstance,
497
- nextChildIDs
498
- );
499
-
500
- let shouldResetChildren = false;
501
- if (prevChildIDs === undefined) {
502
- // We haven't computed children before.
503
- // So we'll have to do it now.
504
- // Next time they'll be cached for comparison
505
- // TODO: this might not make sense. Revisit.
506
- shouldResetChildren = true;
507
- } else if (nextChildIDs.length > 1) {
508
- if (prevChildIDs.length !== nextChildIDs.length) {
509
- shouldResetChildren = true;
510
- } else {
511
- for (let i = 0; i < prevChildIDs.length; i++) {
512
- if (prevChildIDs[i] !== nextChildIDs[i]) {
513
- shouldResetChildren = true;
514
- break;
515
- }
516
- }
517
- }
518
- }
519
- if (shouldResetChildren) {
520
- ops.push(TREE_OPERATION_REORDER_CHILDREN);
521
- ops.push(getID(internalInstance));
522
- ops.push(nextChildIDs.length);
523
- for (let i = 0; i < nextChildIDs.length; i++) {
524
- ops.push(nextChildIDs[i]);
525
- }
467
+ function pushOperation(op: number): void {
468
+ if (__DEV__) {
469
+ if (!Number.isInteger(op)) {
470
+ console.error(
471
+ 'pushOperation() was called but the value is not an integer.',
472
+ op
473
+ );
474
}
527
- });
528
- return ops;
475
+ }
476
+ pendingOperations.push(op);
477
}
478
479
function getStringID(str: string | null): number {
@@ -704,100 +652,6 @@ export function attach(
652
}
653
}
654
707
- function recordPendingMount(internalInstance: InternalInstance) {
708
- const id = getID(internalInstance);
709
- pendingMountIDs.add(id);
710
- idToParentIDMap.set(id, currentParentID);
711
-
712
- if (__DEBUG__) {
713
- console.log(
714
- '%crecordPendingMount()',
715
- 'color: green',
716
- getID(internalInstance)
717
- );
718
- }
719
-
720
- queueFlushPendingEvents();
721
- }
722
-
723
- function recordPendingReorder(internalInstance: InternalInstance) {
724
- const id = getID(internalInstance);
725
- pendingReorderIDs.add(id);
726
-
727
- if (__DEBUG__) {
728
- console.log('%crecordPendingReorder()', 'color: green', id);
729
- }
730
-
731
- queueFlushPendingEvents();
732
- }
733
-
734
- function recordPendingUnmount(internalInstance: InternalInstance) {
735
- const id = getID(internalInstance);
736
-
737
- pendingUnmountIDs.add(id);
738
-
739
- // Not all nodes are mounted (or unmounted) in the frontend DevTools tree,
740
- // so it's important to remove entries from this map on pending unmount.
741
- idToParentIDMap.delete(id);
742
-
743
- if (__DEBUG__) {
744
- console.log(
745
- '%crecordPendingUnmount()',
746
- 'color: red',
747
- getID(internalInstance)
748
- );
749
- }
750
-
751
- queueFlushPendingEvents();
752
- }
753
-
754
- function recordMount(id: number, parentID: number) {
755
- const internalInstance = ((idToInternalInstanceMap.get(
756
- id
757
- ): any): InternalInstance);
758
- const isRoot = rootIDs.has(id);
759
-
760
- if (__DEBUG__) {
761
- console.log(
762
- '%crecordMount()',
763
- 'color: green; font-weight: bold;',
764
- id,
765
- getData(internalInstance).displayName
766
- );
767
- }
768
-
769
- if (isRoot) {
770
- // TODO Is this right? For all versions?
771
- const hasOwnerMetadata =
772
- internalInstance._currentElement != null &&
773
- internalInstance._currentElement._owner != null;
774
-
775
- pushOperation(TREE_OPERATION_ADD);
776
- pushOperation(id);
777
- pushOperation(ElementTypeRoot);
778
- pushOperation(0); // isProfilingSupported?
779
- pushOperation(hasOwnerMetadata ? 1 : 0);
780
- } else {
781
- const { displayName, key, type } = getData(internalInstance);
782
-
783
- const ownerID =
784
- internalInstance._currentElement != null &&
785
- internalInstance._currentElement._owner != null
786
- ? getID(internalInstance._currentElement._owner)
787
- : 0;
788
-
789
- let displayNameStringID = getStringID(displayName);
790
- let keyStringID = getStringID(key);
791
- pushOperation(TREE_OPERATION_ADD);
792
- pushOperation(id);
793
- pushOperation(type);
794
- pushOperation(parentID);
795
- pushOperation(ownerID);
796
- pushOperation(displayNameStringID);
797
- pushOperation(keyStringID);
798
- }
799
- }
800
-
655
function setInProps(id: number, path: Array<string | number>, value: any) {
656
const internalInstance = idToInternalInstanceMap.get(id);
657
if (internalInstance != null) {
src/backend/legacy/utils.js
-14
@@ -2,20 +2,6 @@
2
3
import type { InternalInstance } from './renderer';
4
5
-export function decorateResult(
6
- object: Object,
7
- attr: string,
8
- fn: Function
9
-): Function {
10
- const old = object[attr];
11
- object[attr] = function(instance: InternalInstance) {
12
- const res = old.apply(this, arguments);
13
- fn(res);
14
- return res;
15
- };
16
- return old;
17
-}
18
-
5
export function decorate(object: Object, attr: string, fn: Function): Function {
6
const old = object[attr];
7
object[attr] = function(instance: InternalInstance) {