Fiber side-effects
This adds tracking of side-effects that gets scheduled during an update. As the tree gets reconciled, the side-effectful fibers are linked together in an ordered singly linked list. That way we can walk the linked list to commit only the work that needs to be synchronous - quickly. We also store first and last nodes within a fiber. That way when we reuse an already processed subtree, we can reuse that subset of the linked list.
Sebastian Markbage committed
Jun 29, 2016 at 15:20 UTC
f84a8eabc7618de81dac4bb2b645f85303dc430d
4 files changed
+111
-5
src/renderers/shared/fiber/ReactFiber.js
+21
@@ -82,6 +82,16 @@ export type Fiber = Instance & {
82
// if this returns multiple values. Such as a fragment.
83
output: any, // This type will be more specific once we overload the tag.
84
85
+ // Singly linked list fast path to the next fiber with side-effects.
86
+ nextEffect: ?Fiber,
87
+
88
+ // The first and last fiber with side-effect within this subtree. This allows
89
+ // us to reuse a slice of the linked list when we reuse the work done within
90
+ // this fiber.
91
+ firstEffect: ?Fiber,
92
+ lastEffect: ?Fiber,
93
+
94
+
95
// This will be used to quickly determine if a subtree has no pending changes.
96
pendingWorkPriority: PriorityLevel,
97
@@ -129,6 +139,10 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
139
memoizedProps: null,
140
output: null,
141
142
+ nextEffect: null,
143
+ firstEffect: null,
144
+ lastEffect: null,
145
+
146
pendingWorkPriority: NoWork,
147
148
hasWorkInProgress: false,
@@ -157,6 +171,13 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
171
alt.ref = alt.ref;
172
alt.pendingProps = fiber.pendingProps;
173
alt.pendingWorkPriority = priorityLevel;
174
+
175
+ // Whenever we clone, we do so to get a new work in progress.
176
+ // This ensures that we've reset these in the new tree.
177
+ alt.nextEffect = null;
178
+ alt.firstEffect = null;
179
+ alt.lastEffect = null;
180
+
181
return alt;
182
}
183
src/renderers/shared/fiber/ReactFiberCommitWork.js
new
+38
@@ -0,0 +1,38 @@
1
+/**
2
+ * Copyright 2013-present, Facebook, Inc.
3
+ * All rights reserved.
4
+ *
5
+ * This source code is licensed under the BSD-style license found in the
6
+ * LICENSE file in the root directory of this source tree. An additional grant
7
+ * of patent rights can be found in the PATENTS file in the same directory.
8
+ *
9
+ * @providesModule ReactFiberCommitWork
10
+ * @flow
11
+ */
12
+
13
+'use strict';
14
+
15
+import type { Fiber } from 'ReactFiber';
16
+
17
+var ReactTypeOfWork = require('ReactTypeOfWork');
18
+var {
19
+ ClassComponent,
20
+ HostContainer,
21
+ HostComponent,
22
+} = ReactTypeOfWork;
23
+
24
+exports.commitWork = function(finishedWork : Fiber) : void {
25
+ switch (finishedWork.tag) {
26
+ case ClassComponent:
27
+ // TODO: Fire componentDidMount/componentDidUpdate, update refs
28
+ return;
29
+ case HostContainer:
30
+ // TODO: Attach children to root container.
31
+ return;
32
+ case HostComponent:
33
+ console.log('commit updates to host component', finishedWork.type);
34
+ return;
35
+ default:
36
+ throw new Error('This unit of work tag should not have side-effects.');
37
+ }
38
+};
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+15
@@ -30,6 +30,16 @@ var {
30
YieldComponent,
31
} = ReactTypeOfWork;
32
33
+function markForPostEffect(workInProgress : Fiber) {
34
+ // Schedule a side-effect on this fiber, after the children's side-effects.
35
+ if (workInProgress.lastEffect) {
36
+ workInProgress.lastEffect.nextEffect = workInProgress;
37
+ } else {
38
+ workInProgress.firstEffect = workInProgress;
39
+ }
40
+ workInProgress.lastEffect = workInProgress;
41
+}
42
+
43
function transferOutput(child : ?Fiber, returnFiber : Fiber) {
44
// If we have a single result, we just pass that through as the output to
45
// avoid unnecessary traversal. When we have multiple output, we just pass
@@ -108,6 +118,11 @@ exports.completeWork = function(current : ?Fiber, workInProgress : Fiber) : ?Fib
118
return null;
119
case HostComponent:
120
transferOutput(workInProgress.child, workInProgress);
121
+ if (workInProgress.alternate) {
122
+ // If we have an alternate, that means this is an update and we need to
123
+ // schedule a side-effect to do the updates.
124
+ markForPostEffect(workInProgress);
125
+ }
126
console.log('/host component', workInProgress.type);
127
return null;
128
case CoroutineComponent:
src/renderers/shared/fiber/ReactFiberScheduler.js
+37
-5
@@ -19,6 +19,7 @@ import type { HostConfig } from 'ReactFiberReconciler';
19
var { cloneFiber } = require('ReactFiber');
20
var { beginWork } = require('ReactFiberBeginWork');
21
var { completeWork } = require('ReactFiberCompleteWork');
22
+var { commitWork } = require('ReactFiberCommitWork');
23
var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork');
24
25
var {
@@ -78,6 +79,22 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
79
return null;
80
}
81
82
+ function commitAllWork(finishedWork : Fiber) {
83
+ // Commit all the side-effects within a tree.
84
+ // TODO: Error handling.
85
+ let effectfulFiber = finishedWork.firstEffect;
86
+ while (effectfulFiber) {
87
+ commitWork(effectfulFiber);
88
+ const next = effectfulFiber.nextEffect;
89
+ // Ensure that we clean these up so that we don't accidentally keep them.
90
+ // I'm not actually sure this matters because we can't reset firstEffect
91
+ // and lastEffect since they're on every node, not just the effectful
92
+ // ones. So we have to clean everything as we reuse nodes anyway.
93
+ effectfulFiber.nextEffect = null;
94
+ effectfulFiber = next;
95
+ }
96
+ }
97
+
98
function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
99
while (true) {
100
// The current, flushed, state of this fiber is the alternate.
@@ -96,11 +113,25 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
113
114
const returnFiber = workInProgress.return;
115
99
- // Ensure that remaining work priority bubbles up.
100
- if (returnFiber && workInProgress.pendingWorkPriority !== NoWork &&
101
- (returnFiber.pendingWorkPriority === NoWork ||
102
- returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
103
- returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority;
116
+ if (returnFiber) {
117
+ // Ensure that remaining work priority bubbles up.
118
+ if (workInProgress.pendingWorkPriority !== NoWork &&
119
+ (returnFiber.pendingWorkPriority === NoWork ||
120
+ returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) {
121
+ returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority;
122
+ }
123
+ // Ensure that the first and last effect of the parent corresponds
124
+ // to the children's first and last effect. This probably relies on
125
+ // children completing in order.
126
+ if (!returnFiber.firstEffect) {
127
+ returnFiber.firstEffect = workInProgress.firstEffect;
128
+ }
129
+ if (workInProgress.lastEffect) {
130
+ if (returnFiber.lastEffect) {
131
+ returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
132
+ }
133
+ returnFiber.lastEffect = workInProgress.lastEffect;
134
+ }
135
}
136
137
if (next) {
@@ -121,6 +152,7 @@ module.exports = function<T, P, I>(config : HostConfig<T, P, I>) {
152
// also ensures that work scheduled during reconciliation gets deferred.
153
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
154
console.log('----- COMPLETED with remaining work:', workInProgress.pendingWorkPriority);
155
+ commitAllWork(workInProgress);
156
const nextWork = findNextUnitOfWork();
157
// if (!nextWork && hasMoreWork) {
158
// TODO: This can happen when some deep work completes and we don't