Add some support for reordering
Dan Abramov committed
May 31, 2019 at 18:00 UTC
2794b92164058f38683cad49f04ff801e04ff34b
3 files changed
+166
-11
src/__tests__/legacy/__snapshots__/storeLegacy-v15-test.js.snap
+79
@@ -15,6 +15,32 @@ exports[`Store (legacy) collapseNodesByDefault:false should filter DOM nodes fro
15
<div>
16
`;
17
18
+exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 1: mount 1`] = `
19
+[root]
20
+ ▾ <Root>
21
+ ▾ <div>
22
+ ▾ <Component key="a">
23
+ <div>
24
+`;
25
+
26
+exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 2: add child 1`] = `
27
+[root]
28
+ ▾ <Root>
29
+ ▾ <div>
30
+ ▾ <Component key="a">
31
+ <div>
32
+ ▾ <Component key="b">
33
+ <div>
34
+`;
35
+
36
+exports[`Store (legacy) collapseNodesByDefault:false should support adding and removing children: 3: remove child 1`] = `
37
+[root]
38
+ ▾ <Root>
39
+ ▾ <div>
40
+ ▾ <Component key="b">
41
+ <div>
42
+`;
43
+
44
exports[`Store (legacy) collapseNodesByDefault:false should support collapsing parts of the tree: 1: mount 1`] = `
45
[root]
46
▾ <Grandparent>
@@ -185,6 +211,59 @@ exports[`Store (legacy) collapseNodesByDefault:false should support mount and up
211
212
exports[`Store (legacy) collapseNodesByDefault:false should support mount and update operations: 3: unmount 1`] = ``;
213
214
+exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 1: mount 1`] = `
215
+[root]
216
+ ▾ <Root>
217
+ ▾ <div>
218
+ ▾ <Foo key="foo">
219
+ ▾ <div>
220
+ ▾ <Component key="0">
221
+ <div>
222
+ ▾ <Bar key="bar">
223
+ ▾ <div>
224
+ ▾ <Component key="0">
225
+ <div>
226
+ ▾ <Component key="1">
227
+ <div>
228
+`;
229
+
230
+exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 2: reorder children 1`] = `
231
+[root]
232
+ ▾ <Root>
233
+ ▾ <div>
234
+ ▾ <Bar key="bar">
235
+ ▾ <div>
236
+ ▾ <Component key="0">
237
+ <div>
238
+ ▾ <Component key="1">
239
+ <div>
240
+ ▾ <Foo key="foo">
241
+ ▾ <div>
242
+ ▾ <Component key="0">
243
+ <div>
244
+`;
245
+
246
+exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 3: collapse root 1`] = `
247
+[root]
248
+ ▸ <Root>
249
+`;
250
+
251
+exports[`Store (legacy) collapseNodesByDefault:false should support reordering of children: 4: expand root 1`] = `
252
+[root]
253
+ ▾ <Root>
254
+ ▾ <div>
255
+ ▾ <Bar key="bar">
256
+ ▾ <div>
257
+ ▾ <Component key="0">
258
+ <div>
259
+ ▾ <Component key="1">
260
+ <div>
261
+ ▾ <Foo key="foo">
262
+ ▾ <div>
263
+ ▾ <Component key="0">
264
+ <div>
265
+`;
266
+
267
exports[`Store (legacy) collapseNodesByDefault:true should not filter DOM nodes from the store tree: 1: mount 1`] = `
268
[root]
269
▸ <Grandparent>
src/__tests__/legacy/storeLegacy-v15-test.js
+5
-8
@@ -176,10 +176,9 @@ describe('Store (legacy)', () => {
176
expect(store).toMatchSnapshot('6: expand Grandparent');
177
});
178
179
- // TODO Re-enable this test once the renderer supports it.
180
- xit('should support adding and removing children', () => {
179
+ it('should support adding and removing children', () => {
180
const Root = ({ children }) => <div>{children}</div>;
182
- const Component = () => null;
181
+ const Component = () => <div />;
182
183
const container = document.createElement('div');
184
@@ -215,10 +214,9 @@ describe('Store (legacy)', () => {
214
expect(store).toMatchSnapshot('3: remove child');
215
});
216
218
- // TODO Re-enable this test once the renderer supports it.
219
- xit('should support reordering of children', () => {
217
+ it('should support reordering of children', () => {
218
const Root = ({ children }) => <div>{children}</div>;
221
- const Component = () => null;
219
+ const Component = () => <div />;
220
221
const Foo = () => <div>{[<Component key="0" />]}</div>;
222
const Bar = () => (
@@ -447,10 +445,9 @@ describe('Store (legacy)', () => {
445
expect(store).toMatchSnapshot('6: expand middle node');
446
});
447
450
- // TODO Re-enable this test once the renderer supports it.
448
xit('should support reordering of children', () => {
449
const Root = ({ children }) => <div>{children}</div>;
453
- const Component = () => null;
450
+ const Component = () => <div />;
451
452
const Foo = () => <div>{[<Component key="0" />]}</div>;
453
const Bar = () => (
src/backend/legacy/renderer.js
+82
-3
@@ -12,6 +12,7 @@ import {
12
__DEBUG__,
13
TREE_OPERATION_ADD,
14
TREE_OPERATION_REMOVE,
15
+ TREE_OPERATION_REORDER_CHILDREN,
16
} from '../../constants';
17
import getChildren from './getChildren';
18
import getData from './getData';
@@ -49,6 +50,10 @@ export function attach(
50
const idToInternalInstanceMap: Map<number, InternalInstance> = new Map();
51
const idToParentIDMap: Map<number, number> = new Map();
52
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 {
@@ -217,6 +222,7 @@ export function attach(
222
const result = fn.apply(this, args);
223
currentParentID = prevParentID;
224
225
+ recordPendingReorder(internalInstance);
226
return result;
227
},
228
receiveComponent(fn, args) {
@@ -227,6 +233,7 @@ export function attach(
233
const result = fn.apply(this, args);
234
currentParentID = prevParentID;
235
236
+ recordPendingReorder(internalInstance);
237
return result;
238
},
239
unmountComponent(fn, args) {
@@ -261,6 +268,7 @@ export function attach(
268
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;
@@ -303,7 +311,11 @@ export function attach(
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...
306
- if (pendingMountIDs.size > 0 || pendingUnmountIDs.size > 0) {
314
+ if (
315
+ pendingMountIDs.size > 0 ||
316
+ pendingUnmountIDs.size > 0 ||
317
+ pendingReorderIDs.size > 0
318
+ ) {
319
rootIDs.forEach(flushPendingEvents);
320
}
321
}, 0);
@@ -396,6 +408,7 @@ export function attach(
408
const numUnmountIDs =
409
unmountIDs.length + (pendingUnmountedRootID === null ? 0 : 1);
410
411
+ const reorderOperations = computePendingReorderOperations();
412
const operations = new Uint32Array(
413
// Identify which renderer this update is coming from.
414
2 + // [rendererID, rootFiberID]
@@ -406,8 +419,12 @@ export function attach(
419
// All unmounts are batched in a single message.
420
// [TREE_OPERATION_REMOVE, removedIDLength, ...ids]
421
(numUnmountIDs > 0 ? 2 + numUnmountIDs : 0) +
409
- // Mount/update/reorder operations
410
- pendingOperations.length
422
+ // 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
428
);
429
430
// Identify which renderer this update is coming from.
@@ -444,6 +461,9 @@ export function attach(
461
462
// Fill in the rest of the operations.
463
operations.set(pendingOperations, i);
464
+ i += pendingOperations.length;
465
+
466
+ operations.set(reorderOperations, i);
467
468
if (__DEBUG__) {
469
printOperationsArray(operations);
@@ -454,12 +474,60 @@ export function attach(
474
475
pendingOperations.length = 0;
476
pendingMountIDs.clear();
477
+ pendingReorderIDs.clear();
478
pendingUnmountIDs.clear();
479
pendingUnmountedRootID = null;
480
pendingStringTable.clear();
481
pendingStringTableLength = 0;
482
}
483
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
+ }
526
+ }
527
+ });
528
+ return ops;
529
+ }
530
+
531
function getStringID(str: string | null): number {
532
if (str === null) {
533
return 0;
@@ -652,6 +720,17 @@ export function attach(
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