Handle the case of reparenting while updating children

Instead of trying to flush the detached children from the child list, we keep
the set of detached children up to date and query on every read.
This commit is contained in:
Adam Barth 2016-03-11 12:06:30 -08:00
parent f3dc82abda
commit 9c08836273
2 changed files with 113 additions and 24 deletions

View File

@ -1536,10 +1536,14 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
/// Attempts to update the given old children list using the given new /// Attempts to update the given old children list using the given new
/// widgets, removing obsolete elements and introducing new ones as necessary, /// widgets, removing obsolete elements and introducing new ones as necessary,
/// and then returns the new child list. /// and then returns the new child list.
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets) { List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> detachedChildren }) {
assert(oldChildren != null); assert(oldChildren != null);
assert(newWidgets != null); assert(newWidgets != null);
Element replaceWithNullIfDetached(Element child) {
return detachedChildren != null && detachedChildren.contains(child) ? null : child;
}
// This attempts to diff the new child list (this.children) with // This attempts to diff the new child list (this.children) with
// the old child list (old.children), and update our renderObject // the old child list (old.children), and update our renderObject
// accordingly. // accordingly.
@ -1582,7 +1586,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
// Update the top of the list. // Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
Element oldChild = oldChildren[oldChildrenTop]; Element oldChild = replaceWithNullIfDetached(oldChildren[oldChildrenTop]);
Widget newWidget = newWidgets[newChildrenTop]; Widget newWidget = newWidgets[newChildrenTop];
assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active); assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
@ -1597,7 +1601,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
// Scan the bottom of the list. // Scan the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
Element oldChild = oldChildren[oldChildrenBottom]; Element oldChild = replaceWithNullIfDetached(oldChildren[oldChildrenBottom]);
Widget newWidget = newWidgets[newChildrenBottom]; Widget newWidget = newWidgets[newChildrenBottom];
assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active); assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget)) if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
@ -1612,7 +1616,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
if (haveOldChildren) { if (haveOldChildren) {
oldKeyedChildren = new Map<Key, Element>(); oldKeyedChildren = new Map<Key, Element>();
while (oldChildrenTop <= oldChildrenBottom) { while (oldChildrenTop <= oldChildrenBottom) {
Element oldChild = oldChildren[oldChildrenTop]; Element oldChild = replaceWithNullIfDetached(oldChildren[oldChildrenTop]);
assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active); assert(oldChild == null || oldChild._debugLifecycleState == _ElementLifecycle.active);
if (oldChild != null) { if (oldChild != null) {
if (oldChild.widget.key != null) if (oldChild.widget.key != null)
@ -1663,7 +1667,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
// Update the bottom of the list. // Update the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) { while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
Element oldChild = oldChildren[oldChildrenTop]; Element oldChild = oldChildren[oldChildrenTop];
assert(oldChild != null); assert(replaceWithNullIfDetached(oldChild) != null);
assert(oldChild._debugLifecycleState == _ElementLifecycle.active); assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
Widget newWidget = newWidgets[newChildrenTop]; Widget newWidget = newWidgets[newChildrenTop];
assert(Widget.canUpdate(oldChild.widget, newWidget)); assert(Widget.canUpdate(oldChild.widget, newWidget));
@ -1678,8 +1682,10 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
// clean up any of the remaining middle nodes from the old list // clean up any of the remaining middle nodes from the old list
if (haveOldChildren && oldKeyedChildren.isNotEmpty) { if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
for (Element oldChild in oldKeyedChildren.values) for (Element oldChild in oldKeyedChildren.values) {
_deactivateChild(oldChild); if (detachedChildren == null || !detachedChildren.contains(oldChild))
_deactivateChild(oldChild);
}
} }
return newChildren; return newChildren;
@ -1808,19 +1814,9 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte
} }
List<Element> _children; List<Element> _children;
// We null out detached children lazily to avoid O(n^2) work walking _children // We keep a set of detached children to avoid O(n^2) work walking _children
// repeatedly to remove children. // repeatedly to remove children.
Set<Element> _detachedChildren; final Set<Element> _detachedChildren = new HashSet<Element>();
void _replaceDetachedChildrenWithNull() {
if (_detachedChildren != null && _detachedChildren.isNotEmpty) {
for (int i = 0; i < _children.length; ++i) {
if (_detachedChildren.contains(_children[i]))
_children[i] = null;
}
_detachedChildren.clear();
}
}
void insertChildRenderObject(RenderObject child, Element slot) { void insertChildRenderObject(RenderObject child, Element slot) {
final ContainerRenderObjectMixin renderObject = this.renderObject; final ContainerRenderObjectMixin renderObject = this.renderObject;
@ -1860,15 +1856,13 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte
} }
void visitChildren(ElementVisitor visitor) { void visitChildren(ElementVisitor visitor) {
_replaceDetachedChildrenWithNull();
for (Element child in _children) { for (Element child in _children) {
if (child != null) if (!_detachedChildren.contains(child))
visitor(child); visitor(child);
} }
} }
bool detachChild(Element child) { bool detachChild(Element child) {
_detachedChildren ??= new Set<Element>();
_detachedChildren.add(child); _detachedChildren.add(child);
_deactivateChild(child); _deactivateChild(child);
return true; return true;
@ -1888,8 +1882,8 @@ class MultiChildRenderObjectElement<T extends MultiChildRenderObjectWidget> exte
void update(T newWidget) { void update(T newWidget) {
super.update(newWidget); super.update(newWidget);
assert(widget == newWidget); assert(widget == newWidget);
_replaceDetachedChildrenWithNull(); _children = updateChildren(_children, widget.children, detachedChildren: _detachedChildren);
_children = updateChildren(_children, widget.children); _detachedChildren.clear();
} }
} }

View File

@ -196,4 +196,99 @@ void main() {
expect(keyState.marker, equals("marked")); expect(keyState.marker, equals("marked"));
}); });
}); });
test('Reparent during update children', () {
testWidgets((WidgetTester tester) {
GlobalKey key = new GlobalKey();
tester.pumpWidget(new Stack(
children: <Widget>[
new StateMarker(key: key),
new Container(width: 100.0, height: 100.0),
]
));
StateMarkerState keyState = key.currentState;
keyState.marker = "marked";
tester.pumpWidget(new Stack(
children: <Widget>[
new Container(width: 100.0, height: 100.0),
new StateMarker(key: key),
]
));
expect(key.currentState, equals(keyState));
expect(keyState.marker, equals("marked"));
tester.pumpWidget(new Stack(
children: <Widget>[
new StateMarker(key: key),
new Container(width: 100.0, height: 100.0),
]
));
expect(key.currentState, equals(keyState));
expect(keyState.marker, equals("marked"));
});
});
test('Reparent to child during update children', () {
testWidgets((WidgetTester tester) {
GlobalKey key = new GlobalKey();
tester.pumpWidget(new Stack(
children: <Widget>[
new Container(width: 100.0, height: 100.0),
new StateMarker(key: key),
new Container(width: 100.0, height: 100.0),
]
));
StateMarkerState keyState = key.currentState;
keyState.marker = "marked";
tester.pumpWidget(new Stack(
children: <Widget>[
new Container(width: 100.0, height: 100.0, child: new StateMarker(key: key)),
new Container(width: 100.0, height: 100.0),
]
));
expect(key.currentState, equals(keyState));
expect(keyState.marker, equals("marked"));
tester.pumpWidget(new Stack(
children: <Widget>[
new Container(width: 100.0, height: 100.0),
new StateMarker(key: key),
new Container(width: 100.0, height: 100.0),
]
));
expect(key.currentState, equals(keyState));
expect(keyState.marker, equals("marked"));
tester.pumpWidget(new Stack(
children: <Widget>[
new Container(width: 100.0, height: 100.0),
new Container(width: 100.0, height: 100.0, child: new StateMarker(key: key)),
]
));
expect(key.currentState, equals(keyState));
expect(keyState.marker, equals("marked"));
tester.pumpWidget(new Stack(
children: <Widget>[
new Container(width: 100.0, height: 100.0),
new StateMarker(key: key),
new Container(width: 100.0, height: 100.0),
]
));
expect(key.currentState, equals(keyState));
expect(keyState.marker, equals("marked"));
});
});
} }