Introduce shouldComponentUpdate in Fiber
It is important to be able to use this since it avoids starvation problems if you can reuse partially completed work.
Sebastian Markbage committed
Jul 20, 2016 at 19:34 UTC
94ed00740b191617420f3736dba8df3c0bbeac96
3 files changed
+286
-81
src/renderers/shared/fiber/ReactFiberBeginWork.js
+99
-65
@@ -54,17 +54,35 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
54
workInProgress.pendingWorkPriority = NoWork;
55
}
56
57
- function updateClassComponent(current, workInProgress) {
57
+ function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
58
var props = workInProgress.pendingProps;
59
- if (!workInProgress.stateNode) {
59
+ var instance = workInProgress.stateNode;
60
+ if (!instance) {
61
var ctor = workInProgress.type;
61
- workInProgress.stateNode = new ctor(props);
62
+ workInProgress.stateNode = instance = new ctor(props);
63
+ } else if (typeof instance.shouldComponentUpdate === 'function') {
64
+ if (current && current.memoizedProps) {
65
+ // Revert to the last flushed props, incase we aborted an update.
66
+ instance.props = current.memoizedProps;
67
+ if (!instance.shouldComponentUpdate(props)) {
68
+ return bailoutOnCurrent(current, workInProgress);
69
+ }
70
+ }
71
+ if (!workInProgress.hasWorkInProgress && workInProgress.memoizedProps) {
72
+ // Reset the props, in case this is a ping-pong case rather than a
73
+ // completed update case. For the completed update case, the instance
74
+ // props will already be the memoizedProps.
75
+ instance.props = workInProgress.memoizedProps;
76
+ if (!instance.shouldComponentUpdate(props)) {
77
+ return bailoutOnAlreadyFinishedWork(workInProgress);
78
+ }
79
+ }
80
}
63
- var instance = workInProgress.stateNode;
81
instance.props = props;
82
var nextChildren = instance.render();
83
reconcileChildren(current, workInProgress, nextChildren);
84
workInProgress.pendingWorkPriority = NoWork;
85
+ return workInProgress.child;
86
}
87
88
function updateHostComponent(current, workInProgress) {
@@ -165,90 +183,103 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
183
} while (child = child.sibling);
184
}
185
186
+ function bailoutOnCurrent(current : Fiber, workInProgress : Fiber) : ?Fiber {
187
+ // The most likely scenario is that the previous copy of the tree contains
188
+ // the same props as the new one. In that case, we can just copy the output
189
+ // and children from that node.
190
+ workInProgress.memoizedProps = workInProgress.pendingProps;
191
+ workInProgress.output = current.output;
192
+ const priorityLevel = workInProgress.pendingWorkPriority;
193
+ workInProgress.pendingProps = null;
194
+ workInProgress.pendingWorkPriority = NoWork;
195
+ workInProgress.stateNode = current.stateNode;
196
+ if (current.child) {
197
+ // If we bail out but still has work with the current priority in this
198
+ // subtree, we need to go find it right now. If we don't, we won't flush
199
+ // it until the next tick.
200
+ workInProgress.child = current.child;
201
+ reuseChildren(workInProgress, workInProgress.child);
202
+ if (workInProgress.pendingWorkPriority !== NoWork && workInProgress.pendingWorkPriority <= priorityLevel) {
203
+ // TODO: This passes the current node and reads the priority level and
204
+ // pending props from that. We want it to read our priority level and
205
+ // pending props from the work in progress. Needs restructuring.
206
+ return findNextUnitOfWorkAtPriority(current, priorityLevel);
207
+ } else {
208
+ return null;
209
+ }
210
+ } else {
211
+ workInProgress.child = null;
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function bailoutOnAlreadyFinishedWork(workInProgress : Fiber) : ?Fiber {
217
+ // If we started this work before, and finished it, or if we're in a
218
+ // ping-pong update scenario, this version could already be what we're
219
+ // looking for. In that case, we should be able to just bail out.
220
+ const priorityLevel = workInProgress.pendingWorkPriority;
221
+ workInProgress.pendingProps = null;
222
+ workInProgress.pendingWorkPriority = NoWork;
223
+
224
+ workInProgress.firstEffect = null;
225
+ workInProgress.nextEffect = null;
226
+ workInProgress.lastEffect = null;
227
+
228
+ if (workInProgress.child && workInProgress.child.alternate) {
229
+ // On the way up here, we reset the child node to be the current one.
230
+ // Therefore we have to reuse the alternate. This is super weird.
231
+ let child = workInProgress.child.alternate;
232
+ workInProgress.child = child;
233
+ // Ensure that the effects of reused work are preserved.
234
+ reuseChildrenEffects(workInProgress, child);
235
+ // If we bail out but still has work with the current priority in this
236
+ // subtree, we need to go find it right now. If we don't, we won't flush
237
+ // it until the next tick.
238
+ reuseChildren(workInProgress, child);
239
+ if (workInProgress.pendingWorkPriority !== NoWork &&
240
+ workInProgress.pendingWorkPriority <= priorityLevel) {
241
+ // TODO: This passes the current node and reads the priority level and
242
+ // pending props from that. We want it to read our priority level and
243
+ // pending props from the work in progress. Needs restructuring.
244
+ return findNextUnitOfWorkAtPriority(workInProgress, priorityLevel);
245
+ }
246
+ }
247
+ return null;
248
+ }
249
+
250
function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber {
251
// The current, flushed, state of this fiber is the alternate.
252
// Ideally nothing should rely on this, but relying on it here
253
// means that we don't need an additional field on the work in
254
// progress.
255
if (current && workInProgress.pendingProps === current.memoizedProps) {
174
- // The most likely scenario is that the previous copy of the tree contains
175
- // the same props as the new one. In that case, we can just copy the output
176
- // and children from that node.
177
- workInProgress.memoizedProps = workInProgress.pendingProps;
178
- workInProgress.output = current.output;
179
- const priorityLevel = workInProgress.pendingWorkPriority;
180
- workInProgress.pendingProps = null;
181
- workInProgress.pendingWorkPriority = NoWork;
182
- workInProgress.stateNode = current.stateNode;
183
- if (current.child) {
184
- // If we bail out but still has work with the current priority in this
185
- // subtree, we need to go find it right now. If we don't, we won't flush
186
- // it until the next tick.
187
- workInProgress.child = current.child;
188
- reuseChildren(workInProgress, workInProgress.child);
189
- if (workInProgress.pendingWorkPriority !== NoWork && workInProgress.pendingWorkPriority <= priorityLevel) {
190
- // TODO: This passes the current node and reads the priority level and
191
- // pending props from that. We want it to read our priority level and
192
- // pending props from the work in progress. Needs restructuring.
193
- return findNextUnitOfWorkAtPriority(workInProgress.alternate, priorityLevel);
194
- } else {
195
- return null;
196
- }
197
- } else {
198
- workInProgress.child = null;
199
- return null;
200
- }
256
+ return bailoutOnCurrent(current, workInProgress);
257
}
258
259
if (!workInProgress.hasWorkInProgress &&
260
workInProgress.pendingProps === workInProgress.memoizedProps) {
205
- // If we started this work before, and finished it, or if we're in a
206
- // ping-pong update scenario, this version could already be what we're
207
- // looking for. In that case, we should be able to just bail out.
208
- const priorityLevel = workInProgress.pendingWorkPriority;
209
- workInProgress.pendingProps = null;
210
- workInProgress.pendingWorkPriority = NoWork;
211
-
212
- workInProgress.firstEffect = null;
213
- workInProgress.nextEffect = null;
214
- workInProgress.lastEffect = null;
215
-
216
- if (workInProgress.child && workInProgress.child.alternate) {
217
- // On the way up here, we reset the child node to be the current one.
218
- // Therefore we have to reuse the alternate. This is super weird.
219
- workInProgress.child = workInProgress.child.alternate;
220
- // Ensure that the effects of reused work are preserved.
221
- reuseChildrenEffects(workInProgress, workInProgress.child);
222
- // If we bail out but still has work with the current priority in this
223
- // subtree, we need to go find it right now. If we don't, we won't flush
224
- // it until the next tick.
225
- reuseChildren(workInProgress, workInProgress.child);
226
- if (workInProgress.pendingWorkPriority !== NoWork && workInProgress.pendingWorkPriority <= priorityLevel) {
227
- // TODO: This passes the current node and reads the priority level and
228
- // pending props from that. We want it to read our priority level and
229
- // pending props from the work in progress. Needs restructuring.
230
- return findNextUnitOfWorkAtPriority(workInProgress, priorityLevel);
231
- }
232
- }
233
- return null;
261
+ return bailoutOnAlreadyFinishedWork(workInProgress);
262
}
263
236
- workInProgress.hasWorkInProgress = true;
237
-
264
+ let nextWork;
265
switch (workInProgress.tag) {
266
case IndeterminateComponent:
267
mountIndeterminateComponent(current, workInProgress);
268
+ workInProgress.hasWorkInProgress = true;
269
return workInProgress.child;
270
case FunctionalComponent:
271
updateFunctionalComponent(current, workInProgress);
272
+ workInProgress.hasWorkInProgress = true;
273
return workInProgress.child;
274
case ClassComponent:
246
- updateClassComponent(current, workInProgress);
247
- return workInProgress.child;
275
+ nextWork = updateClassComponent(current, workInProgress);
276
+ workInProgress.hasWorkInProgress = true;
277
+ return nextWork;
278
case HostContainer:
279
reconcileChildren(current, workInProgress, workInProgress.pendingProps);
280
// A yield component is just a placeholder, we can just run through the
281
// next one immediately.
282
+ workInProgress.hasWorkInProgress = true;
283
workInProgress.pendingWorkPriority = NoWork;
284
if (workInProgress.child) {
285
return beginWork(
@@ -258,13 +289,16 @@ module.exports = function<T, P, I, C>(config : HostConfig<T, P, I, C>) {
289
}
290
return null;
291
case HostComponent:
261
- return updateHostComponent(current, workInProgress);
292
+ nextWork = updateHostComponent(current, workInProgress);
293
+ workInProgress.hasWorkInProgress = true;
294
+ return nextWork;
295
case CoroutineHandlerPhase:
296
// This is a restart. Reset the tag to the initial phase.
297
workInProgress.tag = CoroutineComponent;
298
// Intentionally fall through since this is now the same.
299
case CoroutineComponent:
300
updateCoroutineComponent(current, workInProgress);
301
+ workInProgress.hasWorkInProgress = true;
302
// This doesn't take arbitrary time so we could synchronously just begin
303
// eagerly do the work of workInProgress.child as an optimization.
304
if (workInProgress.child) {
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+121
-16
@@ -291,11 +291,16 @@ describe('ReactIncremental', function() {
291
return <div>{props.children}</div>;
292
}
293
294
- function Tester() {
295
- // This component is just here to ensure that the bail out is
296
- // in fact in effect in the expected place for this test.
297
- ops.push('Tester');
298
- return <div />;
294
+ class Tester extends React.Component {
295
+ shouldComponentUpdate() {
296
+ return false;
297
+ }
298
+ render() {
299
+ // This component is just here to ensure that the bail out is
300
+ // in fact in effect in the expected place for this test.
301
+ ops.push('Tester');
302
+ return <div />;
303
+ }
304
}
305
306
function Middle(props) {
@@ -303,23 +308,28 @@ describe('ReactIncremental', function() {
308
return <span>{props.children}</span>;
309
}
310
306
- var middleContent = (
307
- <aaa>
308
- <Tester />
309
- <bbb hidden={true}>
310
- <ccc>
311
- <Middle>Hi</Middle>
312
- </ccc>
313
- </bbb>
314
- </aaa>
315
- );
311
+ class Content extends React.Component {
312
+ shouldComponentUpdate() {
313
+ return false;
314
+ }
315
+ render() {
316
+ return [
317
+ <Tester unused={this.props.unused} />,
318
+ <bbb hidden={true}>
319
+ <ccc>
320
+ <Middle>Hi</Middle>
321
+ </ccc>
322
+ </bbb>,
323
+ ];
324
+ }
325
+ }
326
327
function Foo(props) {
328
ops.push('Foo');
329
return (
330
<div hidden={props.text === 'bar'}>
331
<Bar>{props.text}</Bar>
322
- {middleContent}
332
+ <Content unused={props.text} />
333
<Bar>{props.text}</Bar>
334
</div>
335
);
@@ -434,4 +444,99 @@ describe('ReactIncremental', function() {
444
445
});
446
447
+ it('can reuse work if shouldComponentUpdate is false, after being preempted', function() {
448
+
449
+ var ops = [];
450
+
451
+ function Bar(props) {
452
+ ops.push('Bar');
453
+ return <div>{props.children}</div>;
454
+ }
455
+
456
+ class Middle extends React.Component {
457
+ shouldComponentUpdate(nextProps) {
458
+ return this.props.children !== nextProps.children;
459
+ }
460
+ render() {
461
+ ops.push('Middle');
462
+ return <span>{this.props.children}</span>;
463
+ }
464
+ }
465
+
466
+ class Content extends React.Component {
467
+ shouldComponentUpdate(nextProps) {
468
+ return this.props.step !== nextProps.step;
469
+ }
470
+ render() {
471
+ ops.push('Content');
472
+ return (
473
+ <div>
474
+ <Middle>{this.props.step === 0 ? 'Hi' : 'Hello'}</Middle>
475
+ <Bar>{this.props.step === 0 ? this.props.text : '-'}</Bar>
476
+ <Middle>{this.props.step === 0 ? 'There' : 'World'}</Middle>
477
+ </div>
478
+ );
479
+ }
480
+ }
481
+
482
+ function Foo(props) {
483
+ ops.push('Foo');
484
+ return (
485
+ <div>
486
+ <Bar>{props.text}</Bar>
487
+ <div hidden={true}>
488
+ <Content step={props.step} text={props.text} />
489
+ </div>
490
+ </div>
491
+ );
492
+ }
493
+
494
+ // Init
495
+ ReactNoop.render(<Foo text="foo" step={0} />);
496
+ ReactNoop.flush();
497
+
498
+ expect(ops).toEqual(['Foo', 'Bar', 'Content', 'Middle', 'Bar', 'Middle']);
499
+
500
+ ops = [];
501
+
502
+ // Make a quick update which will schedule low priority work to
503
+ // update the middle content.
504
+ ReactNoop.render(<Foo text="bar" step={1} />);
505
+ ReactNoop.flushLowPri(30);
506
+
507
+ expect(ops).toEqual(['Foo', 'Bar']);
508
+
509
+ ops = [];
510
+
511
+ // The middle content is now pending rendering...
512
+ ReactNoop.flushLowPri(30);
513
+ expect(ops).toEqual(['Content', 'Middle', 'Bar']); // One more Middle left.
514
+
515
+ ops = [];
516
+
517
+ // but we'll interupt it to render some higher priority work.
518
+ // The middle content will bailout so it remains untouched.
519
+ ReactNoop.render(<Foo text="foo" step={1} />);
520
+ ReactNoop.flushLowPri(30);
521
+
522
+ expect(ops).toEqual(['Foo', 'Bar']);
523
+
524
+ ops = [];
525
+
526
+ // Since we did nothing to the middle subtree during the interuption,
527
+ // we should be able to reuse the reconciliation work that we already did
528
+ // without restarting.
529
+ ReactNoop.flush();
530
+ // TODO: Content never fully completed its render so can't completely bail
531
+ // out on the entire subtree. However, we could do a shallow bail out and
532
+ // not rerender Content, but keep going down the incomplete tree.
533
+ // Normally shouldComponentUpdate->false is not enough to determine that we
534
+ // can safely reuse the old props, but I think in this case it would be ok,
535
+ // since it is a resume of already started work.
536
+ // Because of the above we can also not reuse the work of Bar because the
537
+ // rerender of Content will generate a new element which will mean we don't
538
+ // auto-bail out from Bar.
539
+ expect(ops).toEqual(['Content', 'Bar', 'Middle']);
540
+
541
+ });
542
});
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+66
@@ -188,6 +188,72 @@ describe('ReactIncrementalSideEffects', function() {
188
]);
189
});
190
191
+ it('can reuse side-effects after being preempted, if shouldComponentUpdate is false', function() {
192
+
193
+ class Bar extends React.Component {
194
+ shouldComponentUpdate(nextProps) {
195
+ return this.props.children !== nextProps.children;
196
+ }
197
+ render() {
198
+ return <span prop={this.props.children} />;
199
+ }
200
+ }
201
+
202
+ class Content extends React.Component {
203
+ shouldComponentUpdate(nextProps) {
204
+ return this.props.step !== nextProps.step;
205
+ }
206
+ render() {
207
+ return (
208
+ <div>
209
+ <Bar>{this.props.step === 0 ? 'Hi' : 'Hello'}</Bar>
210
+ <Bar>{this.props.step === 0 ? this.props.text : 'World'}</Bar>
211
+ </div>
212
+ );
213
+ }
214
+ }
215
+
216
+ function Foo(props) {
217
+ return (
218
+ <div hidden={true}>
219
+ <Content step={props.step} text={props.text} />
220
+ </div>
221
+ );
222
+ }
223
+
224
+ // Init
225
+ ReactNoop.render(<Foo text="foo" step={0} />);
226
+ ReactNoop.flush();
227
+
228
+ expect(ReactNoop.root.children).toEqual([
229
+ div(div(span('Hi'), span('foo'))),
230
+ ]);
231
+
232
+ // Make a quick update which will schedule low priority work to
233
+ // update the middle content.
234
+ ReactNoop.render(<Foo text="bar" step={1} />);
235
+ ReactNoop.flushLowPri(35);
236
+
237
+ // The tree remains unchanged.
238
+ expect(ReactNoop.root.children).toEqual([
239
+ div(div(span('Hi'), span('foo'))),
240
+ ]);
241
+
242
+ // The first Bar has already completed its update but we'll interupt it to
243
+ // render some higher priority work. The middle content will bailout so
244
+ // it remains untouched which means that it should reuse it next time.
245
+ ReactNoop.render(<Foo text="foo" step={1} />);
246
+ ReactNoop.flush(30);
247
+
248
+ // Since we did nothing to the middle subtree during the interuption,
249
+ // we should be able to reuse the reconciliation work that we already did
250
+ // without restarting. The side-effects should still be replayed.
251
+
252
+ expect(ReactNoop.root.children).toEqual([
253
+ div(div(span('Hello'), span('World'))),
254
+ ]);
255
+ });
256
+
257
it('updates a child even though the old props is empty', function() {
258
function Foo(props) {
259
return (