@samitouri / QOS-React-2 / commits / 9243bc194c

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) {